From 00f0145d84c13c5083dea47e7af98d13df207023 Mon Sep 17 00:00:00 2001 From: Steluro <282664784+Steluro@users.noreply.github.com> Date: Sat, 25 Jul 2026 13:35:13 -0400 Subject: [PATCH 1/3] feat: add an execution history for auditing --- .env.example | 8 +++++ app.py | 4 +++ config.py | 23 +++++++++++++ database.py | 44 +++++++++++++++++++++++++ docker-compose.yaml | 11 +++++++ execution_history/__init__.py | 0 execution_history/history_item.py | 14 ++++++++ execution_history/history_recorder.py | 38 +++++++++++++++++++++ execution_history/history_repository.py | 37 +++++++++++++++++++++ requirements-dev.txt | 11 +++++++ requirements.in | 5 ++- requirements.txt | 12 ++++++- 12 files changed, 205 insertions(+), 2 deletions(-) create mode 100644 .env.example create mode 100644 config.py create mode 100644 database.py create mode 100644 docker-compose.yaml create mode 100644 execution_history/__init__.py create mode 100644 execution_history/history_item.py create mode 100644 execution_history/history_recorder.py create mode 100644 execution_history/history_repository.py diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..c8370b2 --- /dev/null +++ b/.env.example @@ -0,0 +1,8 @@ +# Database Configuration +DB_DIALECT=postgresql +DB_HOST="localhost" +DB_PORT=5432 +DB_NAME=database_name +DB_SCHEMA=semantic_translator +DB_USER="database_user" +DB_PASSWORD="database_password" diff --git a/app.py b/app.py index 9ef078f..af5b8c9 100644 --- a/app.py +++ b/app.py @@ -6,6 +6,8 @@ from flask_cors import CORS from werkzeug.middleware.proxy_fix import ProxyFix +from database import db +from execution_recorder.history_recorder import init_history_recorder from old_request_handler.request_handler import request_handler as old_request_handler from variability_solver.backends.minizinc.backend import GecodeBackend from variability_solver.backends.z3.backend import Z3Backend @@ -16,6 +18,8 @@ CORS(app) app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_prefix=1) +db.open() +init_history_recorder(app) MODEL_LANGUAGES = {CLIFFrontend()} SOLVERS = {Z3Backend(), GecodeBackend()} diff --git a/config.py b/config.py new file mode 100644 index 0000000..1b52b57 --- /dev/null +++ b/config.py @@ -0,0 +1,23 @@ +import os + +from dotenv import load_dotenv + +load_dotenv() + + +def get_env(name: str) -> str: + value = os.getenv(name) + if value is None: + raise RuntimeError(f"Missing required environment variable: {name}") + return value + + +DB_DIALECT = get_env("DB_DIALECT") +DB_HOST = get_env("DB_HOST") +DB_PORT = get_env("DB_PORT") +DB_NAME = get_env("DB_NAME") +DB_SCHEMA = get_env("DB_SCHEMA") +DB_USER = get_env("DB_USER") +DB_PASSWORD = get_env("DB_PASSWORD") + +DB_URL = f"{DB_DIALECT}://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{DB_PORT}/{DB_NAME}?options=-csearch_path%3D{DB_SCHEMA}" diff --git a/database.py b/database.py new file mode 100644 index 0000000..0b59d2a --- /dev/null +++ b/database.py @@ -0,0 +1,44 @@ +from collections.abc import Iterator +from contextlib import contextmanager + +from psycopg import Connection +from psycopg.rows import dict_row +from psycopg_pool import ConnectionPool + +from config import DB_URL + + +class Database: + def __init__( + self, + conninfo: str, + *, + min_size: int = 2, + max_size: int = 10, + ) -> None: + self._pool = ConnectionPool( + conninfo=conninfo, + min_size=min_size, + max_size=max_size, + open=False, + ) + + def open(self) -> None: + self._pool.open() + + def close(self) -> None: + self._pool.close() + + @contextmanager + def connection(self) -> Iterator[Connection]: + with self._pool.connection() as conn: + yield conn + + @contextmanager + def cursor(self) -> Iterator: + with self.connection() as conn: + with conn.cursor(row_factory=dict_row) as cur: + yield cur + + +db = Database(conninfo=DB_URL) diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..6dae700 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,11 @@ +services: + semantic_translator: + build: + context: . + image: semantic_translator + container_name: semantic_translator + ports: + - "5000:5000" + env_file: + - .env + restart: unless-stopped \ No newline at end of file diff --git a/execution_history/__init__.py b/execution_history/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/execution_history/history_item.py b/execution_history/history_item.py new file mode 100644 index 0000000..60ce8f6 --- /dev/null +++ b/execution_history/history_item.py @@ -0,0 +1,14 @@ +from dataclasses import dataclass +from datetime import datetime +from uuid import UUID + + +@dataclass +class HistoryItem: + id: UUID + occured_at: datetime + url: str + request_method: str + request_content: dict | None + response_status: int + response_content: dict | None diff --git a/execution_history/history_recorder.py b/execution_history/history_recorder.py new file mode 100644 index 0000000..c1cd117 --- /dev/null +++ b/execution_history/history_recorder.py @@ -0,0 +1,38 @@ +import uuid +from datetime import UTC, datetime + +from flask import g, request + +from database import db +from execution_recorder.history_item import HistoryItem +from execution_recorder.history_repository import HistoryRepository + + +def init_history_recorder(app): + + @app.before_request + def record_request(): + g.request_id = uuid.uuid4() + g.occurred_at = datetime.now(UTC) + g.request_path = request.path + g.request_method = request.method + g.request_content = request.get_json(silent=True) if request.is_json else None + + @app.after_request + def record_response(response): + g.response_status = response.status_code + g.response_content = response.get_json(silent=True) if response.is_json else None + + HistoryRepository(database=db).save( + HistoryItem( + id=g.request_id, + occured_at=g.occurred_at, + url=g.request_path, + request_method=g.request_method, + request_content=g.request_content, + response_status=g.response_status, + response_content=g.response_content, + ) + ) + + return response diff --git a/execution_history/history_repository.py b/execution_history/history_repository.py new file mode 100644 index 0000000..05b5796 --- /dev/null +++ b/execution_history/history_repository.py @@ -0,0 +1,37 @@ +from psycopg.types.json import Jsonb + +from database import Database + +from .history_item import HistoryItem + + +class HistoryRepository: + def __init__(self, database: Database) -> None: + self._database = database + + def save(self, item: HistoryItem) -> None: + """Save a history item to the database.""" + with self._database.cursor() as cur: + cur.execute( + """ + INSERT INTO history ( + id, + occured_at, + url, + request_method, + request_content, + response_status, + response_content + ) + VALUES (%s, %s, %s, %s, %s, %s, %s) + """, + ( + item.id, + item.occured_at, + item.url, + item.request_method, + Jsonb(item.request_content), + item.response_status, + Jsonb(item.response_content), + ), + ) diff --git a/requirements-dev.txt b/requirements-dev.txt index c1060fc..159f1ad 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -53,6 +53,14 @@ packaging==25.0 # wheel pip-tools==7.6.0 # via -r requirements-dev.in +psycopg[binary]==3.3.4 + # via -r requirements.txt +psycopg-binary==3.3.4 + # via + # -r requirements.txt + # psycopg +psycopg-pool==3.3.1 + # via -r requirements.txt pydantic==1.10.2 # via -r requirements.txt pyhumps==3.8.0 @@ -61,6 +69,8 @@ pyproject-hooks==1.2.0 # via # build # pip-tools +python-dotenv==1.2.2 + # via -r requirements.txt ruff==0.16.0 # via -r requirements-dev.in swiplserver==1.0.2 @@ -70,6 +80,7 @@ textx==4.2.2 typing-extensions==4.14.0 # via # -r requirements.txt + # psycopg-pool # pydantic werkzeug==3.1.3 # via diff --git a/requirements.in b/requirements.in index 3718e4b..43a16c2 100644 --- a/requirements.in +++ b/requirements.in @@ -1,10 +1,13 @@ Flask flask-cors gunicorn -pyhumps minizinc networkx +psycopg[binary] +psycopg-pool pydantic +pyhumps +python-dotenv swiplserver textX z3-solver \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index dfccc11..de46028 100644 --- a/requirements.txt +++ b/requirements.txt @@ -33,16 +33,26 @@ networkx==3.4.2 # via -r requirements.in packaging==25.0 # via gunicorn +psycopg[binary]==3.3.4 + # via -r requirements.in +psycopg-binary==3.3.4 + # via psycopg +psycopg-pool==3.3.1 + # via -r requirements.in pydantic==1.10.2 # via -r requirements.in pyhumps==3.8.0 # via -r requirements.in +python-dotenv==1.2.2 + # via -r requirements.in swiplserver==1.0.2 # via -r requirements.in textx==4.2.2 # via -r requirements.in typing-extensions==4.14.0 - # via pydantic + # via + # psycopg-pool + # pydantic werkzeug==3.1.3 # via flask z3-solver==4.15.1.0 From c53a0471d78da0e59c325281abadcc10861fd36a Mon Sep 17 00:00:00 2001 From: Steluro <282664784+Steluro@users.noreply.github.com> Date: Sat, 25 Jul 2026 13:40:37 -0400 Subject: [PATCH 2/3] patch(imports): rename imports to match actual module name --- app.py | 2 +- execution_history/history_recorder.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app.py b/app.py index af5b8c9..93ba8f0 100644 --- a/app.py +++ b/app.py @@ -7,7 +7,7 @@ from werkzeug.middleware.proxy_fix import ProxyFix from database import db -from execution_recorder.history_recorder import init_history_recorder +from execution_history.history_recorder import init_history_recorder from old_request_handler.request_handler import request_handler as old_request_handler from variability_solver.backends.minizinc.backend import GecodeBackend from variability_solver.backends.z3.backend import Z3Backend diff --git a/execution_history/history_recorder.py b/execution_history/history_recorder.py index c1cd117..8355e13 100644 --- a/execution_history/history_recorder.py +++ b/execution_history/history_recorder.py @@ -4,8 +4,9 @@ from flask import g, request from database import db -from execution_recorder.history_item import HistoryItem -from execution_recorder.history_repository import HistoryRepository + +from .history_item import HistoryItem +from .history_repository import HistoryRepository def init_history_recorder(app): From 4501b1edabf700e5e12222b26ba97b20af1e2160 Mon Sep 17 00:00:00 2001 From: Steluro <282664784+Steluro@users.noreply.github.com> Date: Sat, 25 Jul 2026 13:46:51 -0400 Subject: [PATCH 3/3] patch(.env): change variable name to be usable on remote server with other services --- .env.example | 14 +++++++------- config.py | 14 +++++++------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/.env.example b/.env.example index c8370b2..3b3ca4c 100644 --- a/.env.example +++ b/.env.example @@ -1,8 +1,8 @@ # Database Configuration -DB_DIALECT=postgresql -DB_HOST="localhost" -DB_PORT=5432 -DB_NAME=database_name -DB_SCHEMA=semantic_translator -DB_USER="database_user" -DB_PASSWORD="database_password" +SEMANTIC_TRANSLATOR_DB_DIALECT=postgresql +SEMANTIC_TRANSLATOR_DB_HOST="localhost" +SEMANTIC_TRANSLATOR_DB_PORT=5432 +SEMANTIC_TRANSLATOR_DB_NAME=database_name +SEMANTIC_TRANSLATOR_DB_SCHEMA=semantic_translator +SEMANTIC_TRANSLATOR_DB_USER="database_user" +SEMANTIC_TRANSLATOR_DB_PASSWORD="database_password" diff --git a/config.py b/config.py index 1b52b57..707eb9e 100644 --- a/config.py +++ b/config.py @@ -12,12 +12,12 @@ def get_env(name: str) -> str: return value -DB_DIALECT = get_env("DB_DIALECT") -DB_HOST = get_env("DB_HOST") -DB_PORT = get_env("DB_PORT") -DB_NAME = get_env("DB_NAME") -DB_SCHEMA = get_env("DB_SCHEMA") -DB_USER = get_env("DB_USER") -DB_PASSWORD = get_env("DB_PASSWORD") +DB_DIALECT = get_env("SEMANTIC_TRANSLATOR_DB_DIALECT") +DB_HOST = get_env("SEMANTIC_TRANSLATOR_DB_HOST") +DB_PORT = get_env("SEMANTIC_TRANSLATOR_DB_PORT") +DB_NAME = get_env("SEMANTIC_TRANSLATOR_DB_NAME") +DB_SCHEMA = get_env("SEMANTIC_TRANSLATOR_DB_SCHEMA") +DB_USER = get_env("SEMANTIC_TRANSLATOR_DB_USER") +DB_PASSWORD = get_env("SEMANTIC_TRANSLATOR_DB_PASSWORD") DB_URL = f"{DB_DIALECT}://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{DB_PORT}/{DB_NAME}?options=-csearch_path%3D{DB_SCHEMA}"