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) 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") 73stability_threshold = db.Column(db.Integer, default=15) 74 75def __init__(self, name, owner): 76self.name = name 77self.owner_name = owner.username 78 79class Endpoint(db.Model): 80id = db.Column(db.Integer, unique=True, nullable=False, primary_key=True, autoincrement=True) 81application_id = db.Column(db.Integer, db.ForeignKey("application.id"), nullable=False) 82address = db.Column(db.String(2048), nullable=False) 83name = db.Column(db.String(64), nullable=False) 84comment = db.Column(db.String(2048), nullable=True) 85ping_interval = db.Column(db.Integer, default=300, nullable=False) 86buggy = db.Column(db.Boolean, default=False) 87create_date = db.Column(db.DateTime, default=datetime.datetime.utcnow) 88 89application = db.relationship("Application", back_populates="endpoints") 90statuses = db.relationship("Status", back_populates="endpoint", lazy="dynamic") 91 92def __init__(self, application, name, address, ping_interval, comment=""): 93self.application_id = application.id 94self.name = name 95self.address = address 96self.comment = comment 97self.ping_interval = ping_interval 98 99 100class Status(db.Model): 101id = db.Column(db.Integer, nullable=False, autoincrement=True, primary_key=True) 102endpoint_id = db.Column(db.Integer, db.ForeignKey("endpoint.id"), nullable=False) 103time = db.Column(db.DateTime, default=datetime.datetime.utcnow) 104 105status = db.Column(db.SmallInteger, nullable=False) 106buggy = db.Column(db.Boolean, default=False) 107endpoint = db.relationship("Endpoint", back_populates="statuses") 108 109def __init__(self, endpoint_id, status, buggy): 110self.endpoint_id = endpoint_id 111self.status = status 112self.buggy = buggy 113 114 115@celery.shared_task(name="ping") 116def ping(id, address, next_ping): 117if not db.session.get(Endpoint, id): 118return 119elif db.session.get(Endpoint, id).buggy: 120buggy = True 121else: 122buggy = False 123url = address 124print(f"Pinging {url}") 125response = httpx.get(url, verify=False) 126reading = Status(id, response.status_code, buggy) 127last_reading = db.session.query(Status).filter_by(endpoint_id=id).order_by(Status.time.desc()).first() 128db.session.add(reading) 129db.session.commit() 130 131# Schedule the next ping 132ping.apply_async(args=(id, address, next_ping), countdown=next_ping) 133 134@celery.shared_task(name="ping_all") 135def ping_all(): 136endpoints = Endpoint.query.all() 137for endpoint in endpoints: 138ping.delay(endpoint.id, endpoint.address, endpoint.ping_interval) 139 140 141task = ping_all.delay() 142 143print() 144print() 145print(task) 146print() 147print() 148 149 150@app.context_processor 151def default(): 152return { 153"session": flask.session, 154} 155 156 157@app.route("/") 158def dashboard(): 159current_user = db.session.get(User, flask.session.get("username")) 160return flask.render_template("dashboard.html", apps=Application.query.all(), 161bugs=Endpoint.query.filter_by(buggy=True).filter(Application.owner_name == current_user.username).all() if current_user else []) 162 163 164@app.route("/my") 165def my_apps(): 166if not flask.session.get("username"): 167return flask.redirect("/login", code=303) 168return flask.render_template("my-apps.html", apps=db.session.query(Application).filter_by(owner_name=flask.session["username"]).all()) 169 170 171@app.route("/login", methods=["GET"]) 172def login(): 173return flask.render_template("login.html") 174 175 176@app.route("/signup", methods=["GET"]) 177def signup(): 178return flask.render_template("signup.html") 179 180 181@app.route("/new-app", methods=["GET"]) 182def new_app(): 183if not flask.session.get("username"): 184return flask.redirect("/login", code=303) 185return flask.render_template("new-app.html") 186 187 188@app.route("/new-app", methods=["POST"]) 189def new_app_post(): 190if not flask.session.get("username"): 191return flask.redirect("/login", code=303) 192if Application.query.filter_by(name=flask.request.form["name"]).first(): 193flask.flash("Application already exists") 194return flask.redirect("/new-app", code=303) 195 196new_app_ = Application( 197flask.request.form["name"], 198db.session.get(User, flask.session["username"]), 199) 200db.session.add(new_app_) 201db.session.commit() 202return flask.redirect("/", code=303) 203 204 205@app.route("/login", methods=["POST"]) 206def login_post(): 207user = db.session.get(User, flask.request.form["username"]) 208if not user: 209flask.flash("Username doesn't exist") 210return flask.redirect("/signup", code=303) 211if not bcrypt.check_password_hash(user.password, flask.request.form["password"]): 212flask.flash("Wrong password") 213return flask.redirect("/signup", code=303) 214 215flask.session["username"] = user.username 216return flask.redirect("/", code=303) 217 218 219@app.route("/logout") 220def logout(): 221flask.session.pop("username", None) 222return flask.redirect("/", code=303) 223 224 225@app.route("/signup", methods=["POST"]) 226def signup_post(): 227if flask.request.form["password"] != flask.request.form["password2"]: 228flask.flash("Passwords do not match") 229return flask.redirect("/signup", code=303) 230if db.session.get(User, flask.request.form["username"]): 231flask.flash("Username already exists") 232return flask.redirect("/signup", code=303) 233if len(flask.request.form["password"]) < 8: 234flask.flash("Password must be at least 8 characters") 235return flask.redirect("/signup", code=303) 236if len(flask.request.form["username"]) < 4: 237flask.flash("Username must be at least 4 characters") 238return flask.redirect("/signup", code=303) 239 240new_user = User( 241flask.request.form["username"], 242flask.request.form["password"], 243) 244db.session.add(new_user) 245db.session.commit() 246flask.session["username"] = new_user.username 247return flask.redirect("/", code=303) 248 249 250# UTC filter 251@app.template_filter("utc") 252def utc_filter(timestamp): 253return datetime.datetime.utcfromtimestamp(timestamp) 254 255 256@app.route("/app/<int:app_id>/") 257def app_info(app_id): 258app_ = db.session.get(Application, app_id) 259 260time_slices = [(datetime.datetime.utcnow() - datetime.timedelta(minutes=int(flask.request.args.get("bar_duration", 30)) * (i+1)), 261datetime.datetime.utcnow() - datetime.timedelta(minutes=int(flask.request.args.get("bar_duration", 30)) * i)) 262for i in range(int(flask.request.args.get("time_period", 30)) // int(flask.request.args.get("bar_duration", 1)), 0, -1)] 263 264slice_results = {} 265all_results = [] 266 267for endpoint in app_.endpoints: 268slice_results[endpoint.id] = [] 269 270for slice_ in time_slices: 271slice_results[endpoint.id].append( 272( 273db.session.query(Status).filter( 274sqlalchemy.and_(Status.endpoint_id == endpoint.id, 275Status.time >= slice_[0], 276Status.time < slice_[1])).all(), 277slice_ 278) 279) 280 281all_results.extend(db.session.query(Status).filter( 282sqlalchemy.and_(Status.endpoint_id == endpoint.id, 283Status.time >= datetime.datetime.utcnow() - datetime.timedelta(minutes=app_.stability_threshold), 284Status.time < datetime.datetime.utcnow())).all()) 285 286return flask.render_template("app.html", app=app_, sorted=sorted, list=list, 287sorting=lambda x: x.time, reverse=True, 288is_ok=lambda x: all(status.status in (200, 201, 202, 203, 204, 205, 206, 207, 208, 226, 302, 304, 307) 289for status in x), and_=sqlalchemy.and_, 290is_partial=lambda x: any(status.status in (200, 201, 202, 203, 204, 205, 206, 207, 208, 226, 302, 304, 307) 291for status in x), 292bar_duration=int(flask.request.args.get("bar_duration", 30)), int=int, Status=Status, 293time_period=int(flask.request.args.get("time_period", 1440)), 294now=round(datetime.datetime.utcnow().timestamp()), func=sqlalchemy.func, 295reversed=reversed, fromtimestamp=datetime.datetime.utcfromtimestamp, 296slices=slice_results, bugs=lambda x: any(status.buggy for status in x), 297all_results=all_results) 298 299 300@app.route("/app/<int:app_id>/edit/") 301def app_editor(app_id): 302if flask.session.get("username") != db.session.get(Application, app_id).owner_name: 303flask.abort(403) 304app_ = db.session.get(Application, app_id) 305return flask.render_template("app-editor.html", app=app_) 306 307 308@app.route("/app/<int:app_id>/edit/", methods=["POST"]) 309def app_editor_post(app_id): 310if flask.session.get("username") != db.session.get(Application, app_id).owner_name: 311flask.abort(403) 312app_ = db.session.get(Application, app_id) 313if flask.request.form.get("delete") == "delete": 314endpoints = db.session.query(Endpoint).filter_by(application_id=app_id).all() 315for endpoint in endpoints: 316statuses = db.session.query(Status).filter_by(endpoint_id=endpoint.id).all() 317for status in statuses: 318db.session.delete(status) 319db.session.delete(endpoint) 320db.session.delete(app_) 321db.session.commit() 322else: 323app_.name = flask.request.form["name"] 324app_.stability_threshold = min(300, int(flask.request.form["threshold"])) 325db.session.commit() 326return flask.redirect("/", code=303) 327 328 329@app.route("/app/<int:app_id>/edit/<int:endpoint_id>", methods=["POST"]) 330def endpoint_edit(app_id, endpoint_id): 331if flask.session.get("username") != db.session.get(Application, app_id).owner_name: 332flask.abort(403) 333endpoint = db.session.get(Endpoint, endpoint_id) 334if flask.request.form.get("delete") == "delete": 335statuses = db.session.query(Status).filter_by(endpoint_id=endpoint_id).all() 336for status in statuses: 337db.session.delete(status) 338db.session.delete(endpoint) 339db.session.commit() 340else: 341endpoint.name = flask.request.form["name"] 342endpoint.address = flask.request.form["url"] 343endpoint.ping_interval = max(15, int(flask.request.form["ping_interval"])) 344endpoint.comment = flask.request.form["comment"] 345db.session.commit() 346return flask.redirect(f"/app/{app_id}/edit", code=303) 347 348 349@app.route("/app/<int:app_id>/report/<int:endpoint_id>") 350def endpoint_report(app_id, endpoint_id): 351endpoint = db.session.get(Endpoint, endpoint_id) 352endpoint.buggy = True 353db.session.commit() 354return flask.redirect(f"/app/{app_id}", code=303) 355 356 357@app.route("/app/<int:app_id>/fix/<int:endpoint_id>") 358def endpoint_fix(app_id, endpoint_id): 359if flask.session.get("username") != db.session.get(Application, app_id).owner_name: 360flask.abort(403) 361endpoint = db.session.get(Endpoint, endpoint_id) 362endpoint.buggy = False 363db.session.commit() 364return flask.redirect(f"/app/{app_id}", code=303) 365 366 367@app.route("/app/<int:app_id>/add-endpoint", methods=["POST"]) 368def app_add_endpoint(app_id): 369if flask.session.get("username") != db.session.get(Application, app_id).owner_name: 370flask.abort(403) 371app_ = db.session.get(Application, app_id) 372endpoint = Endpoint(app_, 373flask.request.form["name"], 374flask.request.form["url"], 375max(15, int(flask.request.form["ping_interval"])), 376flask.request.form["comment"]) 377db.session.add(endpoint) 378db.session.commit() 379 380ping.delay(endpoint.id, endpoint.address, endpoint.ping_interval) 381 382return flask.redirect(f"/app/{app_id}/edit", code=303) 383 384