From fe33d714bda1cc1616bb0af4153987a5dbf2e6c5 Mon Sep 17 00:00:00 2001 From: hansi Date: Sat, 29 Nov 2025 00:15:41 +0100 Subject: [PATCH] add logging and database migration --- README.md | 150 +++++++++++++++++- app/logging_config.py | 24 +++ app/main.py | 10 ++ app/migrate.py | 84 ++++++++++ .../0000_initial.sql => app/tools/__init__.py | 0 app/version.py | 1 + data/app.db | Bin 0 -> 24576 bytes migrations/0010_add_users.sql | 0 migrations/0015_add_users_cols.sql | 0 migrations/20251128_210200_initial.sql | 20 +++ 10 files changed, 288 insertions(+), 1 deletion(-) create mode 100644 app/logging_config.py create mode 100644 app/migrate.py rename migrations/0000_initial.sql => app/tools/__init__.py (100%) create mode 100644 app/version.py create mode 100644 data/app.db delete mode 100644 migrations/0010_add_users.sql delete mode 100644 migrations/0015_add_users_cols.sql create mode 100644 migrations/20251128_210200_initial.sql diff --git a/README.md b/README.md index b275a9e..0b79f56 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,151 @@ # co_app -Controlling-App \ No newline at end of file +## Insallation + +1. Git-Repository clonen +2. + +## Entwicklungs- und Produktionsumgebung steuern (APP_ENV) +Die App unterscheidet zwischen Entwicklung und Produktion über die Umgebungsvariable APP_ENV. + +### Entwicklung (lokal) +Im Terminal setzen: +```bash +export APP_ENV=dev +``` +Die Variable gilt, solange das Terminal geöffnet ist. +Beim Starten der App: +```bash +streamlit run app/main.py +``` +Die App erkennt automatisch den Dev-Modus. + +### Produktion (Systemd-Dienst) + +Für den produktiven Betrieb setzt systemd die Variable dauerhaft auf prod: +```ini +Environment=APP_ENV=prod +``` +In prod-Modus laufen z. B. Logging und Pfade anders als lokal. + +### systemd-Dienst einrichten +``/etc/systemd/system/myapp.service`` +```ini +[Unit] +Description=MyApp Streamlit Service +After=network.target + +[Service] +User=co_app +Group=co_app +WorkingDirectory=/opt/co_app +Environment=APP_ENV=prod +ExecStart=/usr/bin/python3 -m streamlit run app/main.py --server.port=8501 +Restart=on-failure + +[Install] +WantedBy=multi-user.target +``` +**Installation & Start** +```bash +sudo systemctl daemon-reload +sudo systemctl enable co_app.service +sudo systemctl start co_app.service +``` +**Logs ansehen** +Da streamlit + python ins systemd-Journal loggen: +```bash +journalctl -u co_app.service -f +``` +**Wichtig** +- APP_ENV=dev → Entwicklung, lokale Logs, lokale DB +- APP_ENV=prod → Systemd-Betrieb, serverseitige Pfade, Logging ins Journal +- Die App liest die Variable mit: +```python +import os +APP_ENV = os.environ.get("APP_ENV", "dev") +``` + +## Database Setup & Migration Guide + +Dieses Projekt verwendet SQLite für die interne App-Datenbank. +Das Schema wird vollständig über SQL-Migrationsdateien verwaltet, die automatisch in der richtigen Reihenfolge ausgeführt werden. + +### Ordnerstruktur + +```bash +data/ ← enthält die erzeugte SQLite-Datenbank (app.db) +migrations/ ← enthält alle SQL-Migrationsdateien +migrate.py ← Python-Skript zum Anwenden der Migrationen +``` +### Erstellen der Datenbank (Initial Setup) + +```bash +python migrate.py +``` +Das Skript: + +1. erstellt den Ordner db/ (falls nicht vorhanden) +2. erzeugt eine neue Datei db/app.db +3. führt alle SQL-Dateien im Ordner migrations/ der Reihe nach aus +4. protokolliert die angewendeten Migrationen in der Tabelle schema_version + +Danach ist die Datenbank vollständig initialisiert und einsatzbereit. + +### Arbeiten mit Migrationen (Änderungen am Schema) + +Änderungen an der Datenbankstruktur werden nie direkt an der DB vorgenommen. +Stattdessen erzeugst du eine neue Migrationsdatei im Ordner migrations/. + +**Beispiel: neue Spalte hinzufügen** + +Lege eine Datei an + +```bash +touch /migrations/20250301_101500_add_last_login.sql +``` +Dateiinhalt +```sql +BEGIN; + +ALTER TABLE users ADD COLUMN last_login DATETIME; + +INSERT INTO schema_version (version) +VALUES ('20250301_101500_add_last_login'); + +COMMIT; +``` +Migration anwenden +```bash +python migrate.py +``` +Das Skript erkennt automatisch, dass diese Migration neu ist und führt sie aus. + +### Erneutes Ausführen (Updates) +Wenn eine bestehende Installation aktualisiert werden soll: +```bash +git pull +python migrate.py +``` +Das Skript: + +- liest die Tabelle schema_version +- führt nur Migrationen aus, die noch nicht angewendet wurden + +Bestehende Strukturen bleiben unberührt. + +### Entwicklungsphase: Hinweise + +Solange das Projekt noch nicht produktiv genutzt wird, gilt: +- du kannst Migrationsdateien löschen/zusammenführen +- du kannst die komplette app.db jederzeit löschen +- vor dem ersten echten Deploy kannst du alle Migrationen zu einer sauberen initial.sql zusammenfassen + +👉 **Nach dem ersten produktiven Einsatz** werden Migrationen nie mehr gelöscht, sondern nur ergänzt. + +### Optional: DB neu erzeugen für Tests +Um eine frische Datenbank zu bekommen: +```bash +rm -f data/app.db +python migrate.py +``` diff --git a/app/logging_config.py b/app/logging_config.py new file mode 100644 index 0000000..7caca38 --- /dev/null +++ b/app/logging_config.py @@ -0,0 +1,24 @@ +import logging +from pathlib import Path + +def setup_logging(app_env: str): + logger = logging.getLogger() + logger.setLevel(logging.INFO) + + fmt = logging.Formatter("%(asctime)s [%(levelname)s] %(message)s") + + # Always log to console (terminal / systemd) + sh = logging.StreamHandler() + sh.setFormatter(fmt) + logger.addHandler(sh) + + # Dev → additional file logging + if app_env == "dev": + log_dir = Path("logs") + log_dir.mkdir(exist_ok=True) + fh = logging.FileHandler(log_dir / "app.log", encoding="utf-8") + fh.setFormatter(fmt) + logger.addHandler(fh) + + return logger + diff --git a/app/main.py b/app/main.py index e69de29..58d55d1 100644 --- a/app/main.py +++ b/app/main.py @@ -0,0 +1,10 @@ +from version import __version__ +from logging_config import setup_logging +import os +from migrate import apply_migrations + +APP_ENV = os.environ.get("APP_ENV", "dev") +logger = setup_logging(APP_ENV) +logger.info(f"Starting app in {APP_ENV} mode - APP-Version {__version__}") + +apply_migrations() \ No newline at end of file diff --git a/app/migrate.py b/app/migrate.py new file mode 100644 index 0000000..04247c6 --- /dev/null +++ b/app/migrate.py @@ -0,0 +1,84 @@ +import sqlite3 +from version import __version__ +from pathlib import Path +import logging +from logging_config import setup_logging +import os + +APP_ENV = os.environ.get("APP_ENV", "dev") +logger = setup_logging(APP_ENV) + +logger.info(f"Starting migration - APP-Version {__version__}") + +logger = logging.getLogger(__name__) + + +BASE_DIR = Path(__file__).resolve().parents[1] +DB_DIR = BASE_DIR / "data" +DB_PATH = DB_DIR / "app.db" +MIGRATIONS_DIR = BASE_DIR / "migrations" + + + +def get_connection() -> sqlite3.Connection: + DB_DIR.mkdir(exist_ok=True) + conn = sqlite3.connect(DB_PATH) + return conn + +def get_applied_versions(conn: sqlite3.Connection) -> set[str]: + """ + Ließt aus schema_version, welche Migrationen schon gelaufen sind. + Falls die Tabelle noch nicht existiert → leeres Set. + """ + try: + cur = conn.execute("SELECT version FROM schema_version") + rows = cur.fetchall() + return {r[0] for r in rows} + except sqlite3.OperationalError: + # Tabelle existiert noch nicht (z.B. bei initialer DB) + + return set() + +def apply_migrations(): + if not MIGRATIONS_DIR.exists(): + raise SystemExit(f"Migrations-Ordner nicht gefunden: {MIGRATIONS_DIR}") + + conn = get_connection() + + try: + applied = get_applied_versions(conn) + + # Alle .sql-Dateien alphabetisch sortiert + sql_files = sorted(MIGRATIONS_DIR.glob("*.sql")) + + if not sql_files: + logger.info("Keine Migrationsdateien gefunden.") + return + + for path in sql_files: + version = path.stem # z.B. '20250220_120000_initial' + + if version in applied: + logger.info(f"[SKIP] {version} (bereits angewendet)") + continue + + logger.info(f"[APPLY] {version}") + sql = path.read_text(encoding="utf-8") + + try: + # Eine Transaktion pro Datei + with conn: + conn.executescript(sql) + except Exception as e: + logger.info(f"Fehler in Migration {version}: {e}") + # hier nicht weiter machen + raise + logger.info(f"Migrationen abgeschlossen. DB: {DB_PATH}") + #print(f"Migrationen abgeschlossen. DB: {DB_PATH}") + + finally: + conn.close() + + +if __name__ == "__main__": + apply_migrations() \ No newline at end of file diff --git a/migrations/0000_initial.sql b/app/tools/__init__.py similarity index 100% rename from migrations/0000_initial.sql rename to app/tools/__init__.py diff --git a/app/version.py b/app/version.py new file mode 100644 index 0000000..b3c06d4 --- /dev/null +++ b/app/version.py @@ -0,0 +1 @@ +__version__ = "0.0.1" \ No newline at end of file diff --git a/data/app.db b/data/app.db new file mode 100644 index 0000000000000000000000000000000000000000..1a9750e1bf6a5c4f497eed55e8540016a552d13f GIT binary patch literal 24576 zcmeI&&yLbS90%|j{;iTA-kNAk<_66!hITdCEHM$Z8e$iAu|zpFm5$N`TA;->#tZR5 zd=Q_)hw!d9kN(=S#*z@dF8Th_VLI*nXy?;jhT-Lf=|xh!4VE(}636T|&O4YAd??mIJiM49UPQ~p3v z6#n^D@|~+tF2m%bkjYODiF#R3c&_O7EaSN`5a)yLS$i-NFN~3B53PQ;M_Fe^&pJwa zx(?mncqyq8mhsvWy*`zpX^N-Dv-Z%mL_O{jc{5qB`N3sPRnx@ya`7#uS;JoUVrcwH zPP@#VFuV+wu03(WiRhU9&Q^zIFx_?4+PjquqguJFs)dsBNa4)$UHM^ywEk_QB}TPrTXKEoH_P;^6~;d%(QYDyX?6Q;wCR0;9rdC;GFq<>Pgq8Irx9N zmDN_Rr1aHYL&@HRt!3MJdd+UcuUdr)uYO~+As_$&2tWV=5P$##AOHafKmY;|xGn+> zo^KuNP5q&!=_j_XHT7oG_Ixk$oN0Wxp=k~MMCkgduAMd?lxbYN`km2+fB*y_009U< z00Izz00bZa0SG|gIt$#%w{GtENg!VTr$7B8AOHafKmY;|fB*y_009U<00I!$V}W@6 zAM^h`e!v(S1Rwwb2tWV=5P$##AOHafK!60W{*TuH0uX=z1Rwwb2tWV=5P$##Ah7=e FzX2K9-qHX7 literal 0 HcmV?d00001 diff --git a/migrations/0010_add_users.sql b/migrations/0010_add_users.sql deleted file mode 100644 index e69de29..0000000 diff --git a/migrations/0015_add_users_cols.sql b/migrations/0015_add_users_cols.sql deleted file mode 100644 index e69de29..0000000 diff --git a/migrations/20251128_210200_initial.sql b/migrations/20251128_210200_initial.sql new file mode 100644 index 0000000..01d6c45 --- /dev/null +++ b/migrations/20251128_210200_initial.sql @@ -0,0 +1,20 @@ + +BEGIN; + +CREATE TABLE IF NOT EXISTS schema_version ( + version TEXT PRIMARY KEY, + applied_at TEXT NOT NULL DEFAULT (datetime('now')) +); + +CREATE TABLE users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + date_create TEXT NOT NULL DEFAULT (datetime('now')), + username TEXT UNIQUE NOT NULL, + password_hash BLOB NOT NULL, + role TEXT NOT NULL DEFAULT 'guest' + +); + +INSERT INTO schema_version (version) VALUES ('20251128_210200_initial'); + +COMMIT;