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