app.py
Python script, ASCII text executable
1import datetime 2 3import celery 4import flask 5import sqlalchemy 6from flask_sqlalchemy import SQLAlchemy 7from flask_bcrypt import Bcrypt 8from flask_migrate import Migrate 9 10from sqlalchemy.orm import declarative_base 11from celery import shared_task 12import httpx 13 14app = flask.Flask(__name__) 15from celery import Celery, Task 16 17 18def celery_init_app(app_) -> Celery: 19class FlaskTask(Task): 20def __call__(self, *args: object, **kwargs: object) -> object: 21with app_.app_context(): 22return self.run(*args, **kwargs) 23 24celery_app = Celery(app_.name, task_cls=FlaskTask) 25celery_app.config_from_object(app_.config["CELERY"]) 26celery_app.set_default() 27app_.extensions["celery"] = celery_app 28return celery_app 29 30 31app.config.from_mapping( 32CELERY=dict( 33broker_url="redis://localhost:6379", 34result_backend="redis://localhost:6379" 35), 36) 37celery_app = celery_init_app(app) 38 39app.config["SQLALCHEMY_DATABASE_URI"] = \ 40"postgresql://echo:1234@localhost:5432/echo" 41app.config["SQLALCHEMY_ENGINE_OPTIONS"] = { 42"connect_args": { 43"options": "-c timezone=utc" 44} 45} 46db = SQLAlchemy(app) 47bcrypt = Bcrypt(app) 48migrate = Migrate(app, db) 49app.config["SESSION_TYPE"] = "filesystem" 50app.config["SECRET_KEY"] = "super secret" 51 52with (app.app_context()): 53class User(db.Model): 54username = db.Column(db.String(64), unique=True, nullable=False, primary_key=True) 55password = db.Column(db.String(72), nullable=False) 56admin = db.Column(db.Boolean, nullable=False, default=False) 57 58applications = db.relationship("Application", back_populates="owner") 59 60def __init__(self, username, password, admin=False): 61self.username = username 62self.password = bcrypt.generate_password_hash(password).decode("utf-8") 63self.admin = admin 64 65class Application(db.Model): 66id = db.Column(db.Integer, primary_key=True, autoincrement=True, unique=True, default=0) 67name = db.Column(db.String(64), unique=True, nullable=False) 68owner_name = db.Column(db.String(64), db.ForeignKey("user.username"), nullable=False) 69 70owner = db.relationship("User", back_populates="applications") 71 72endpoints = db.relationship("Endpoint", back_populates="application") 73 74def __init__(self, name, owner): 75self.name = name 76self.owner_name = owner.username 77 78class Endpoint(db.Model): 79id = db.Column(db.Integer, unique=True, nullable=False, primary_key=True, autoincrement=True) 80application_id = db.Column(db.Integer, db.ForeignKey("application.id"), nullable=False) 81address = db.Column(db.String(2048), nullable=False) 82name = db.Column(db.String(64), nullable=False) 83comment = db.Column(db.String(2048), nullable=True) 84ping_interval = db.Column(db.Integer, default=300, nullable=False) 85buggy = db.Column(db.Boolean, default=False) 86create_date = db.Column(db.DateTime, default=datetime.datetime.utcnow) 87 88application = db.relationship("Application", back_populates="endpoints") 89statuses = db.relationship("Status", back_populates="endpoint", lazy="dynamic") 90 91def __init__(self, application, name, address, ping_interval, comment=""): 92self.application_id = application.id 93self.name = name 94self.address = address 95self.comment = comment 96self.ping_interval = ping_interval 97 98 99class Status(db.Model): 100id = db.Column(db.Integer, nullable=False, autoincrement=True, primary_key=True) 101endpoint_id = db.Column(db.Integer, db.ForeignKey("endpoint.id"), nullable=False) 102time = db.Column(db.DateTime, default=datetime.datetime.utcnow) 103 104status = db.Column(db.SmallInteger, nullable=False) 105buggy = db.Column(db.Boolean, default=False) 106endpoint = db.relationship("Endpoint", back_populates="statuses") 107 108def __init__(self, endpoint_id, status, buggy): 109self.endpoint_id = endpoint_id 110self.status = status 111self.buggy = buggy 112 113 114@celery.shared_task(name="ping") 115def ping(id, address, next_ping): 116if not db.session.get(Endpoint, id): 117return 118elif db.session.get(Endpoint, id).buggy: 119buggy = True 120else: 121buggy = False 122url = address 123print(f"Pinging {url}") 124response = httpx.get(url, verify=False) 125reading = Status(id, response.status_code, buggy) 126last_reading = db.session.query(Status).filter_by(endpoint_id=id).order_by(Status.time.desc()).first() 127db.session.add(reading) 128db.session.commit() 129 130# Schedule the next ping 131ping.apply_async(args=(id, address, next_ping), countdown=next_ping) 132 133@celery.shared_task(name="ping_all") 134def ping_all(): 135endpoints = Endpoint.query.all() 136for endpoint in endpoints: 137ping.delay(endpoint.id, endpoint.address, endpoint.ping_interval) 138 139 140task = ping_all.delay() 141 142print() 143print() 144print(task) 145print() 146print() 147 148 149@app.context_processor 150def default(): 151return { 152"session": flask.session, 153} 154 155 156@app.route("/") 157def dashboard(): 158return flask.render_template("dashboard.html", apps=Application.query.all()) 159 160 161@app.route("/my") 162def my_apps(): 163if not flask.session.get("username"): 164return flask.redirect("/login", code=303) 165return flask.render_template("my-apps.html", apps=db.session.query(Application).filter_by(owner_name=flask.session["username"]).all()) 166 167 168@app.route("/login", methods=["GET"]) 169def login(): 170return flask.render_template("login.html") 171 172 173@app.route("/signup", methods=["GET"]) 174def signup(): 175return flask.render_template("signup.html") 176 177 178@app.route("/new-app", methods=["GET"]) 179def new_app(): 180if not flask.session.get("username"): 181return flask.redirect("/login", code=303) 182return flask.render_template("new-app.html") 183 184 185@app.route("/new-app", methods=["POST"]) 186def new_app_post(): 187if not flask.session.get("username"): 188return flask.redirect("/login", code=303) 189if Application.query.filter_by(name=flask.request.form["name"]).first(): 190flask.flash("Application already exists") 191return flask.redirect("/new-app", code=303) 192 193new_app_ = Application( 194flask.request.form["name"], 195db.session.get(User, flask.session["username"]), 196) 197db.session.add(new_app_) 198db.session.commit() 199return flask.redirect("/", code=303) 200 201 202@app.route("/login", methods=["POST"]) 203def login_post(): 204user = db.session.get(User, flask.request.form["username"]) 205if not user: 206flask.flash("Username doesn't exist") 207return flask.redirect("/signup", code=303) 208if not bcrypt.check_password_hash(user.password, flask.request.form["password"]): 209flask.flash("Wrong password") 210return flask.redirect("/signup", code=303) 211 212flask.session["username"] = user.username 213return flask.redirect("/", code=303) 214 215 216@app.route("/logout") 217def logout(): 218flask.session.pop("username", None) 219return flask.redirect("/", code=303) 220 221 222@app.route("/signup", methods=["POST"]) 223def signup_post(): 224if flask.request.form["password"] != flask.request.form["password2"]: 225flask.flash("Passwords do not match") 226return flask.redirect("/signup", code=303) 227if db.session.get(User, flask.request.form["username"]): 228flask.flash("Username already exists") 229return flask.redirect("/signup", code=303) 230if len(flask.request.form["password"]) < 8: 231flask.flash("Password must be at least 8 characters") 232return flask.redirect("/signup", code=303) 233if len(flask.request.form["username"]) < 4: 234flask.flash("Username must be at least 4 characters") 235return flask.redirect("/signup", code=303) 236 237new_user = User( 238flask.request.form["username"], 239flask.request.form["password"], 240) 241db.session.add(new_user) 242db.session.commit() 243flask.session["username"] = new_user.username 244return flask.redirect("/", code=303) 245 246 247# UTC filter 248@app.template_filter("utc") 249def utc_filter(timestamp): 250return datetime.datetime.utcfromtimestamp(timestamp) 251 252 253@app.route("/app/<int:app_id>/") 254def app_info(app_id): 255app_ = db.session.get(Application, app_id) 256 257time_slices = [(datetime.datetime.utcnow() - datetime.timedelta(minutes=int(flask.request.args.get("bar_duration", 30)) * (i+1)), 258datetime.datetime.utcnow() - datetime.timedelta(minutes=int(flask.request.args.get("bar_duration", 30)) * i)) 259for i in range(int(flask.request.args.get("time_period", 30)) // int(flask.request.args.get("bar_duration", 1)), 0, -1)] 260 261slice_results = {} 262all_results = [] 263 264for endpoint in app_.endpoints: 265slice_results[endpoint.id] = [] 266 267for slice_ in time_slices: 268slice_results[endpoint.id].append( 269( 270db.session.query(Status).filter( 271sqlalchemy.and_(Status.endpoint_id == endpoint.id, 272Status.time >= slice_[0], 273Status.time < slice_[1])).all(), 274slice_ 275) 276) 277 278for endpoint in app_.endpoints: 279all_results.extend(db.session.query(Status).filter( 280sqlalchemy.and_(Status.endpoint_id == endpoint.id, 281Status.time >= datetime.datetime.utcnow() - datetime.timedelta(minutes=10), 282Status.time < datetime.datetime.utcnow())).all()) 283 284return flask.render_template("app.html", app=app_, sorted=sorted, list=list, 285sorting=lambda x: x.time, reverse=True, 286is_ok=lambda x: all(status.status in (200, 201, 202, 203, 204, 205, 206, 207, 208, 226, 302, 304, 307) 287for status in x), and_=sqlalchemy.and_, 288is_partial=lambda x: any(status.status in (200, 201, 202, 203, 204, 205, 206, 207, 208, 226, 302, 304, 307) 289for status in x), 290bar_duration=int(flask.request.args.get("bar_duration", 30)), int=int, Status=Status, 291time_period=int(flask.request.args.get("time_period", 1440)), 292now=round(datetime.datetime.utcnow().timestamp()), func=sqlalchemy.func, 293reversed=reversed, fromtimestamp=datetime.datetime.utcfromtimestamp, 294slices=slice_results, bugs=lambda x: any(status.buggy for status in x), 295all_results=all_results) 296 297 298@app.route("/app/<int:app_id>/edit/") 299def app_editor(app_id): 300if flask.session.get("username") != db.session.get(Application, app_id).owner_name: 301flask.abort(403) 302app_ = db.session.get(Application, app_id) 303return flask.render_template("app-editor.html", app=app_) 304 305 306@app.route("/app/<int:app_id>/edit/<int:endpoint_id>", methods=["POST"]) 307def endpoint_edit(app_id, endpoint_id): 308if flask.session.get("username") != db.session.get(Application, app_id).owner_name: 309flask.abort(403) 310endpoint = db.session.get(Endpoint, endpoint_id) 311if flask.request.form.get("delete") == "delete": 312statuses = db.session.query(Status).filter_by(endpoint_id=endpoint_id).all() 313for status in statuses: 314db.session.delete(status) 315db.session.delete(endpoint) 316db.session.commit() 317else: 318endpoint.name = flask.request.form["name"] 319endpoint.address = flask.request.form["url"] 320endpoint.ping_interval = max(15, int(flask.request.form["ping_interval"])) 321endpoint.comment = flask.request.form["comment"] 322db.session.commit() 323return flask.redirect(f"/app/{app_id}/edit", code=303) 324 325 326@app.route("/app/<int:app_id>/report/<int:endpoint_id>") 327def endpoint_report(app_id, endpoint_id): 328endpoint = db.session.get(Endpoint, endpoint_id) 329endpoint.buggy = True 330db.session.commit() 331return flask.redirect(f"/app/{app_id}", code=303) 332 333 334@app.route("/app/<int:app_id>/fix/<int:endpoint_id>") 335def endpoint_fix(app_id, endpoint_id): 336if flask.session.get("username") != db.session.get(Application, app_id).owner_name: 337flask.abort(403) 338endpoint = db.session.get(Endpoint, endpoint_id) 339endpoint.buggy = False 340db.session.commit() 341return flask.redirect(f"/app/{app_id}", code=303) 342 343 344@app.route("/app/<int:app_id>/add-endpoint", methods=["POST"]) 345def app_add_endpoint(app_id): 346if flask.session.get("username") != db.session.get(Application, app_id).owner_name: 347flask.abort(403) 348app_ = db.session.get(Application, app_id) 349endpoint = Endpoint(app_, 350flask.request.form["name"], 351flask.request.form["url"], 352max(15, int(flask.request.form["ping_interval"])), 353flask.request.form["comment"]) 354db.session.add(endpoint) 355db.session.commit() 356 357ping.delay(endpoint.id, endpoint.address, endpoint.ping_interval) 358 359return flask.redirect(f"/app/{app_id}/edit", code=303) 360 361