Repository navigation
Expand file tree
/
Copy pathapp.py
More file actions
123 lines (95 loc) · 4.47 KB
/
Copy pathapp.py
File metadata and controls
123 lines (95 loc) · 4.47 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
"""Flask application for managing vacation activities backed by PostgreSQL."""
import datetime
import hashlib
import logging
import os
from typing import List, Tuple
from flask import Flask, flash, jsonify, redirect, render_template, request, url_for
from database import PostgresClient
app: Flask = Flask(__name__)
app.secret_key = os.environ.get("SECRET_KEY", os.urandom(24))
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
)
logging.getLogger("urllib3").setLevel(logging.WARNING)
logging.getLogger("werkzeug").setLevel(logging.INFO)
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
# The only difference from the web-app-postgresql-flexible-server sample this app is derived from.
# Every setting below still arrives as a plain environment variable; what changed is where those values
# come from. The Azure App Configuration Kubernetes Provider reads the store, resolves the Key Vault
# references, and writes a ConfigMap and a Secret that the pod consumes with envFrom, so the application
# needs no Azure SDK and no credential of its own.
#
# CONFIG_VERSION is the store's refresh sentinel. Logging it at startup makes a configuration rollout
# observable: bump the key, the provider updates the ConfigMap within its refresh interval, and the new
# value shows up here after `kubectl rollout restart` (environment variables are immutable, so the
# restart is what actually moves it into the process).
logger.info("Configuration version: %s", os.environ.get("CONFIG_VERSION", "n/a"))
def make_activity_id(username: str, activity: str) -> str:
"""MD5 of username + activity + timestamp — preserves the source sample's ID scheme."""
timestamp = datetime.datetime.now().isoformat()
return hashlib.md5(f"{username}_{activity}_{timestamp}".encode()).hexdigest()
db_client: PostgresClient = PostgresClient.from_env()
db_client.init_schema()
username = os.environ.get("LOGIN_NAME", "paolo")
if not username or not username.strip():
raise ValueError("LOGIN_NAME cannot be empty")
activities: List[Tuple[str, str]] = []
def reload_activities() -> None:
activities.clear()
try:
activities.extend(db_client.list_activities(username))
except Exception as exc:
logger.error("Failed to load activities: %s", exc)
@app.route("/", methods=["GET", "POST"])
def index():
if request.method == "POST":
activity = request.form.get("activity")
if activity:
try:
row_id = request.form.get("row_id")
if row_id:
if not row_id.strip():
raise ValueError("Row ID cannot be empty")
if db_client.update_activity(row_id, activity):
logger.info("Activity updated: %s", row_id)
flash("Activity updated!")
else:
activity_id = make_activity_id(username, activity)
db_client.insert_activity(activity_id, username, activity)
logger.info("Activity added: %s", activity)
flash("Activity added!")
except (ConnectionError, ValueError) as e:
logger.error("Error writing activity: %s", e)
return redirect(url_for("index"))
reload_activities()
return render_template("index.html", activities=activities, username=username)
@app.route("/favicon.ico")
def favicon():
return app.send_static_file("favicon.ico")
@app.route("/delete/<string:activity_id>", methods=["POST"])
def delete(activity_id: str):
"""Delete the activity with this id.
The row id addresses the activity, never its position in the rendered page: every replica reloads the
table on each GET, so the list can change between rendering a page and submitting a delete from it, and
a position would then delete whatever activity happens to sit there now.
"""
if activity_id:
db_client.delete_activity(activity_id)
flash("Activity deleted.")
return redirect(url_for("index"))
@app.route("/health")
def health():
"""Liveness and readiness probe: reports whether PostgreSQL answers SELECT 1."""
try:
db_client.ping()
return jsonify({"status": "ok"})
except Exception as exc:
logger.warning("Health check failed: %s", exc)
return jsonify({"status": "unavailable"}), 503
debug = os.environ.get("DEBUG", "false").lower() == "true"
reload_activities()
if __name__ == "__main__":
app.run(debug=debug)