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