From be5400d34948dc406e9da28d6c840fd1852cbfaa Mon Sep 17 00:00:00 2001 From: smile Date: Sat, 6 Jun 2026 12:04:59 +0200 Subject: [PATCH] phase 1 completed --- Dockerfile | 9 +- MIGRATION_PLAN.md | 223 ++++++++++++++++++ {cteward-ng => cteward_ng}/README.md | 0 {cteward-ng => cteward_ng}/__init__.py | 0 .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 202 bytes cteward_ng/__pycache__/app.cpython-313.pyc | Bin 0 -> 7440 bytes cteward_ng/__pycache__/config.cpython-313.pyc | Bin 0 -> 1752 bytes .../__pycache__/memberdata.cpython-313.pyc | Bin 0 -> 4237 bytes {cteward-ng => cteward_ng}/app.py | 78 +++++- {cteward-ng => cteward_ng}/auth.py | 0 {cteward-ng => cteward_ng}/config.py | 0 {cteward-ng => cteward_ng}/database.py | 0 {cteward-ng => cteward_ng}/filters.py | 0 {cteward-ng => cteward_ng}/mappings.py | 0 {cteward-ng => cteward_ng}/memberdata.py | 2 +- {cteward-ng => cteward_ng}/permissions.py | 0 {cteward-ng => cteward_ng}/pytest.ini | 2 - {cteward-ng => cteward_ng}/renderers.py | 0 {cteward-ng => cteward_ng}/requirements.txt | 0 {cteward-ng => cteward_ng}/tests/__init__.py | 0 .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 148 bytes .../conftest.cpython-313-pytest-9.0.3.pyc | Bin 0 -> 1029 bytes .../test_config.cpython-313-pytest-9.0.3.pyc | Bin 0 -> 8716 bytes ...st_memberdata.cpython-313-pytest-9.0.3.pyc | Bin 0 -> 17956 bytes {cteward-ng => cteward_ng}/tests/conftest.py | 0 .../tests/test_config.py | 12 +- .../tests/test_memberdata.py | 2 +- {cteward-ng => cteward_ng}/views.py | 0 podman-compose.yml | 14 +- 29 files changed, 318 insertions(+), 24 deletions(-) create mode 100644 MIGRATION_PLAN.md rename {cteward-ng => cteward_ng}/README.md (100%) rename {cteward-ng => cteward_ng}/__init__.py (100%) create mode 100644 cteward_ng/__pycache__/__init__.cpython-313.pyc create mode 100644 cteward_ng/__pycache__/app.cpython-313.pyc create mode 100644 cteward_ng/__pycache__/config.cpython-313.pyc create mode 100644 cteward_ng/__pycache__/memberdata.cpython-313.pyc rename {cteward-ng => cteward_ng}/app.py (53%) rename {cteward-ng => cteward_ng}/auth.py (100%) rename {cteward-ng => cteward_ng}/config.py (100%) rename {cteward-ng => cteward_ng}/database.py (100%) rename {cteward-ng => cteward_ng}/filters.py (100%) rename {cteward-ng => cteward_ng}/mappings.py (100%) rename {cteward-ng => cteward_ng}/memberdata.py (98%) rename {cteward-ng => cteward_ng}/permissions.py (100%) rename {cteward-ng => cteward_ng}/pytest.ini (64%) rename {cteward-ng => cteward_ng}/renderers.py (100%) rename {cteward-ng => cteward_ng}/requirements.txt (100%) rename {cteward-ng => cteward_ng}/tests/__init__.py (100%) create mode 100644 cteward_ng/tests/__pycache__/__init__.cpython-313.pyc create mode 100644 cteward_ng/tests/__pycache__/conftest.cpython-313-pytest-9.0.3.pyc create mode 100644 cteward_ng/tests/__pycache__/test_config.cpython-313-pytest-9.0.3.pyc create mode 100644 cteward_ng/tests/__pycache__/test_memberdata.cpython-313-pytest-9.0.3.pyc rename {cteward-ng => cteward_ng}/tests/conftest.py (100%) rename {cteward-ng => cteward_ng}/tests/test_config.py (82%) rename {cteward-ng => cteward_ng}/tests/test_memberdata.py (97%) rename {cteward-ng => cteward_ng}/views.py (100%) diff --git a/Dockerfile b/Dockerfile index 8a6123b..b76b3f4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -19,10 +19,13 @@ RUN echo "deb [arch=amd64 signed-by=/usr/share/keyrings/microsoft-prod.gpg] http RUN apt-get update && \ ACCEPT_EULA=Y apt-get install -y msodbcsql18 -RUN pip install --no-cache-dir pyodbc +COPY cteward_ng/requirements.txt /tmp/requirements.txt +RUN pip install --no-cache-dir -r /tmp/requirements.txt WORKDIR /app -COPY . . +COPY cteward_ng/ ./ -CMD ["python", "main.py"] +EXPOSE 5000 + +CMD ["gunicorn", "--bind", "0.0.0.0:5000", "--workers", "2", "--timeout", "120", "app:create_app()"] diff --git a/MIGRATION_PLAN.md b/MIGRATION_PLAN.md new file mode 100644 index 0000000..8c969ea --- /dev/null +++ b/MIGRATION_PLAN.md @@ -0,0 +1,223 @@ +# Migration Plan: cteward-st-lexware (Node.js) → cteward-ng (Python/Flask) + +## Framework Decision + +**Chosen: Flask** — not Django. + +| Criteria | Flask | Django | +|---|---|---| +| App type | Read-only REST API, no admin panel needed | Full-featured framework with ORM, admin, batteries included | +| Complexity | Lightweight, minimal boilerplate | Heavy, opinionated, unnecessary overhead | +| SQL Server access | `pyodbc` works cleanly | Django's MSSQL ORM support is third-party (`mssql-django`) and fragile | +| Existing pattern | Already using `pyodbc` directly in `main.py` | Would require Django models | +| Deployment | Docker-compatible, simple WSGI | Heavier deployment | +| Learning curve | Low (simple routing + middlewares) | High (models, views, templates, settings, etc.) | + +The app is a thin read-only REST API wrapper around MSSQL queries. Flask is the right tool — you get routing, middleware, JSON responses, and extension points without ORM/admin overhead you'll never use. + +--- + +## Architecture Overview of the Legacy App + +``` +HTTP Request (restify) + → Auth Middleware (LDAP or bot password, Basic auth) + → Permission Resolution (flag-based: _board_, _member_, _self_, etc.) + → SQL Query Execution (mssql → MSSQL via connection pool) + → Data Filter (e.g. active-only, self-only) + → Data Mapping (raw DB columns → API response shape) + → Renderer (JSON or CSV output) + → HTTP Response +``` + +--- + +## Phase 0: Project Scaffolding ✅ DONE + +Created `cteward-ng/cteward-ng/` with the following structure: + +``` +cteward-ng/ + cteward-st-lexware/ # ← old, untouched + cteward_ng/ + __init__.py + app.py # Flask app factory + middleware + config.py # Config loading (JSON, env vars) + auth.py # Basic auth + LDAP + bot auth (stubs) + permissions.py # Flag-based permission resolution (stubs) + database.py # pyodbc pool + all SQL query defs (stubs) + memberdata.py # realstatus(), datum(), patenarray() (full) + mappings.py # Raw DB → API response transformers (stubs) + filters.py # Active-only, self-only filters (full) + views.py # Route handlers (stubs) + renderers.py # JSON / CSV response helpers (full) + requirements.txt + pytest.ini + README.md + tests/ + __init__.py + conftest.py # pytest fixtures + test_memberdata.py + test_config.py +``` + +--- + +## Phase 1: Infrastructure & Configuration ✅ DONE + +- [x] **Config loading**: Done in `config.py` — ports the JSON config loading from `st-lexware-test.json` pattern (mssql creds, auth bots, LDAP, logging) +- [x] **Logging**: Replaced `bunyan` with Python's `logging` module + `BunyanFormatter` that produces JSON-structured output matching bunyan format (`name`, `hostname`, `pid`, `level`, `msg`, `time`, `v`) +- [x] **Docker**: Updated `Dockerfile` with Flask + dependencies (`pyodbc`, `ldap3`, `Flask`, `gunicorn`, `DBUtils`). Updated `podman-compose.yml` with proper environment variables, volumes, and restart policy. + +--- + +## Phase 2: Database Layer + +- [ ] **Connection pool**: Port `database.init()` from `mssql`/`tedious` to `pyodbc` with a proper connection pool (use `DBUtils.PooledDB` or SQLAlchemy Core pool). The existing `main.py` has a basic `pyodbc` connection to build on. +- [ ] **Health check**: Port `checkBackendOkay()` → `/legacy/monitor` +- [ ] **Query execution**: Port `runquery()` with parameterized queries. All 14 SQL statements need to be ported from T-SQL `@param` syntax to pyodbc `?` syntax: + - `QUERY_CONTRACTLIST_BY_CREWNAME` ✅ (definition stubbed) + - `QUERY_CONTRACT_BY_CREWNAME_AND_CONTRACT` ✅ + - `QUERY_DEBITLIST_BY_CREWNAME` ✅ + - `QUERY_DEBIT_BY_CREWNAME_AND_GUID` ✅ + - `QUERY_MEMBERLIST` ✅ + - `QUERY_MEMBERLIST_RAW` ✅ + - `QUERY_MEMBER_BY_CREWNAME` ✅ + - `QUERY_MEMBER_MEMO_BY_CREWNAME` ✅ + - `QUERY_WITHDRAWALLIST_BY_CREWNAME` ✅ + - `QUERY_WITHDRAWAL_BY_CREWNAME_AND_GUID` ✅ + - `QUERY_PAYMENTLIST_BY_CREWNAME` ✅ + - `QUERY_STATS_MEMBERS` (special, complex aggregation) ⬅️ needs implementation + - `QUERY_STATS_CONTRACTS` (special) ⬅️ needs implementation + - `QUERY_STATS_GENDERS` (special) ⬅️ needs implementation + - `QUERY_STATS_AGES` (special) ⬅️ needs implementation + +--- + +## Phase 3: Data Utilities + +- [x] **Port `memberdata.js`** → `memberdata.py`: + - [x] `realstatus()` — determine crew/passive/ex-crew/raumfahrer status + - [x] `datum()` — parse `YYYYMMDD` strings to German date format + - [x] `datum_parsed()` — parse ISO date strings + - [x] `patenarray()` / `cleanpaten()` — comma-separated name parsing + +--- + +## Phase 4: Authentication & Authorization + +- [ ] **Port `authprovider.js`** → `auth.py`: + - [x] `check_password()` — plaintext path done, apr1 MD5 hash verification needs `passlib` + - [ ] `find_botuser()` — bot user lookup from config + - [ ] `find_ldapuser()` — LDAP authentication (use `ldap3` Python library instead of `ldapauth-fork`) + - [ ] Basic auth extraction from `Authorization` header (partially done in `app.py` for logging) +- [ ] **Port permission resolution** → `permissions.py`: + - [ ] `find_config_flags()` — flag assignment from config + - [ ] `find_database_flags()` — DB-based flags (_member_, _astronaut_, _passive_) + - [ ] `impersonate()` — `?impersonate=` query param support + - [ ] `effective_permissions()` — lowest-level permission wins + +--- + +## Phase 5: Filters & Mappings + +- [ ] **Port `filters.js`** → `filters.py`: + - [x] `MEMBERLIST_ACTIVE_ONLY` — filter to active members (done, with lazy import) + - [x] `MEMBERLIST_SELF_ONLY` — filter to requesting user only (done) + - [x] `runfilter()` — apply configured filter (done) +- [ ] **Port `mappings.js`** → `mappings.py` (largest file, ~420 lines): + - [x] `NONE` — identity mapper (done) + - [ ] `CONTRACT` — single contract data transformation + - [ ] `CONTRACTLIST` — paginated contract list + - [ ] `DEBIT` — single debit data + - [ ] `DEBITLIST` — paginated debit list + - [ ] `CONTRIBUTIONS` — aggregated contribution summaries (complex) + - [ ] `MEMBER` — full member record (with board-only memo link) + - [ ] `MEMO` — RTF parsing (need Python RTF library, e.g., `rtfparse`) + - [ ] `MEMBERLIST` — paginated member list + - [ ] `MEMBERLIST_TO_LDAPCSV` — CSV export format + - [ ] `WITHDRAWAL` — single withdrawal data + - [ ] `WITHDRAWALLIST` — paginated withdrawal list + +--- + +## Phase 6: API Routes + +- [ ] **Port `startup.js` routes** → `views.py` (Flask blueprints): + - [x] `GET /legacy/monitor` — health check (returns OK placeholder) + - [ ] `GET /legacy/memberlist-oldformat` — CSV member list (LDAP export) + - [ ] `GET /legacy/stats/members` — member count over time + - [ ] `GET /legacy/stats/contracts` — contract statistics + - [ ] `GET /legacy/stats/genders` — gender demographics + - [ ] `GET /legacy/stats/ages` — age demographics + - [ ] `GET /legacy/member/` — member details or list + - [ ] `GET /legacy/member//raw` — raw DB record + - [ ] `GET /legacy/member//memo` — RTF memo + - [ ] `GET /legacy/member//contributions` — contribution summary + - [ ] `GET /legacy/member///[]/raw/` — raw detail records + +--- + +## Phase 7: Response Rendering + +- [x] **Port `renderers.js`** → `renderers.py`: + - [x] `JSON_OUTPUT` — JSON with 2-decimal float formatting + JSONP callback support + - [x] `CSV_OUTPUT` — semicolon-delimited CSV + +--- + +## Phase 8: Middleware + +- [x] **Port request middleware** (partially done in `app.py`): + - [x] Authorization header parsing + username extraction for logging + - [x] `WWW-Authenticate` header on unauthenticated requests + - [x] CORS / gzip (using `flask-compress` + `flask-cors`) + +--- + +## Phase 9: Tests + +- [ ] **Port Mocha tests** to `pytest`: + - [ ] `test/000-startup.js` → app startup + logging test + - [ ] `test/authprovider-*.js` → auth unit tests (6 files) + - [x] `test/memberdata_*.js` → memberdata unit tests (4 files merged into `test_memberdata.py`) + - [ ] `test/legacy_monitor.js` → health check integration test + - Use `pytest-fixtures` for DB mocking, `responses` or `requests-mock` for HTTP + +--- + +## Phase 10: Validation & Cutover + +- [ ] **API parity testing**: Hit every endpoint on both old and new with identical credentials; diff JSON responses byte-for-byte +- [ ] **Deployment**: Update `podman-compose.yml` to point to new Python service, test in staging, cutover + +--- + +## Key Migration Notes + +| Concern | Details | +|---|---| +| **RTF parsing** | `unrtf` (JS) → need Python equivalent. `rtfparse` or `extract-msg` may work. This is the riskiest conversion. | +| **LDAP** | `ldapauth-fork` → `ldap3`. `ldap3` is the standard Python LDAP library. | +| **Password hashing** | `apache-md5` → `passlib` for `apr1` MD5 crypt. | +| **Connection pooling** | Use `DBUtils.PooledDB` with `pyodbc` to match the `mssql` pool behavior. | +| **JSONP** | The callback parameter for JSONP is legacy but must be preserved. | +| **Config format** | Keep the same JSON config format so the deployment doesn't need reconfiguring. | + +--- + +## Estimated Effort + +| Phase | Complexity | Status | +|---|---|---| +| 0. Scaffolding | Trivial | ✅ Done | +| 1. Infrastructure | Low | ⬜ Pending | +| 2. Database Layer | Medium | ⬜ Pending | +| 3. Data Utilities | Low | ✅ Done | +| 4. Auth & Permissions | Medium | ⬜ Pending | +| 5. Filters & Mappings | High (big file) | ✅ Partial (filters done, mappings stubbed) | +| 6. API Routes | Medium | ⬜ Pending | +| 7. Response Rendering | Low | ✅ Done | +| 8. Middleware | Low | ✅ Done (BunyanFormatter, WWW-Authenticate, CORS, gzip) | +| 9. Tests | High | ⬜ Partial (memberdata + config tests done) | +| 10. Validation | Medium | ⬜ Pending | diff --git a/cteward-ng/README.md b/cteward_ng/README.md similarity index 100% rename from cteward-ng/README.md rename to cteward_ng/README.md diff --git a/cteward-ng/__init__.py b/cteward_ng/__init__.py similarity index 100% rename from cteward-ng/__init__.py rename to cteward_ng/__init__.py diff --git a/cteward_ng/__pycache__/__init__.cpython-313.pyc b/cteward_ng/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..69ba70b15ebf95e811255a0820711456e674d9f2 GIT binary patch literal 202 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 O2N&Fa$;_Q$@A`i?I2?Nb literal 0 HcmV?d00001 diff --git a/cteward_ng/__pycache__/config.cpython-313.pyc b/cteward_ng/__pycache__/config.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ac9f7a1e861852d5be113be3ff94084cba703d7e GIT binary patch literal 1752 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 literal 0 HcmV?d00001 diff --git a/cteward-ng/app.py b/cteward_ng/app.py similarity index 53% rename from cteward-ng/app.py rename to cteward_ng/app.py index ce4fe39..18be137 100644 --- a/cteward-ng/app.py +++ b/cteward_ng/app.py @@ -1,7 +1,11 @@ """Flask application factory and middleware setup.""" import base64 +import json import logging +import os +import socket +from datetime import datetime, timezone from logging.handlers import RotatingFileHandler from flask import Flask, request, Response @@ -10,6 +14,58 @@ from flask_compress import Compress from .config import load_config +# Bunyan level mapping +_BUNYAN_LEVELS = { + logging.DEBUG: 20, + logging.INFO: 30, + logging.WARNING: 40, + logging.ERROR: 50, + logging.CRITICAL: 60, +} + +# Reverse mapping: string name → Python level +_LEVEL_NAMES = { + 'trace': logging.DEBUG, + 'debug': logging.DEBUG, + 'info': logging.INFO, + 'warn': logging.WARNING, + 'error': logging.ERROR, + 'fatal': logging.CRITICAL, +} + + +class BunyanFormatter(logging.Formatter): + """Produce bunyan-style JSON log lines. + + Each log line is a single JSON object with keys: + name, hostname, pid, level, msg, time, v + Matches the format expected by the existing test suite. + """ + + def __init__(self): + super().__init__() + self.hostname = socket.gethostname() + self.pid = os.getpid() + + def format(self, record): + log_entry = { + 'name': 'cteward-st-lexware', + 'hostname': self.hostname, + 'pid': self.pid, + 'level': _BUNYAN_LEVELS.get(record.levelno, 30), + 'msg': record.getMessage(), + 'time': datetime.now(timezone.utc).isoformat(), + 'v': 0, + } + # Attach request context if available + if hasattr(record, 'username'): + log_entry['username'] = record.username + if hasattr(record, 'method'): + log_entry['method'] = record.method + if hasattr(record, 'url'): + log_entry['url'] = record.url + return json.dumps(log_entry) + def create_app(config_path=None): """Create and configure the Flask application. @@ -34,24 +90,22 @@ def create_app(config_path=None): def _setup_logging(app): """Setup structured JSON logging similar to bunyan.""" - log_level = app.cteward_config.get('loglevel', 'info').upper() + log_level_name = app.cteward_config.get('loglevel', 'info').lower() logfile = app.cteward_config.get('logfile') + log_level = _LEVEL_NAMES.get(log_level_name, logging.INFO) + handler = ( RotatingFileHandler(logfile) if logfile else logging.StreamHandler() ) - handler.setLevel(getattr(logging, log_level, logging.INFO)) - - formatter = logging.Formatter( - '%(asctime)s %(name)s %(levelname)s %(message)s' - ) - handler.setFormatter(formatter) + handler.setLevel(log_level) + handler.setFormatter(BunyanFormatter()) app.logger.handlers.clear() app.logger.addHandler(handler) - app.logger.setLevel(handler.level) + app.logger.setLevel(log_level) def _setup_extensions(app): @@ -81,7 +135,13 @@ def _register_prehandlers(app): @app.before_request def log_request(): username = _extract_basic_username(request.headers) - app.logger.info('%s %s %s', username, request.method, request.url) + extra = {'username': username, 'method': request.method, 'url': request.url} + # Attach extra fields to the log record so BunyanFormatter picks them up + app.logger.info( + '%s %s %s', username, request.method, request.url, + extra=extra, + extra_data=extra, + ) @app.after_request def www_authenticate(response): diff --git a/cteward-ng/auth.py b/cteward_ng/auth.py similarity index 100% rename from cteward-ng/auth.py rename to cteward_ng/auth.py diff --git a/cteward-ng/config.py b/cteward_ng/config.py similarity index 100% rename from cteward-ng/config.py rename to cteward_ng/config.py diff --git a/cteward-ng/database.py b/cteward_ng/database.py similarity index 100% rename from cteward-ng/database.py rename to cteward_ng/database.py diff --git a/cteward-ng/filters.py b/cteward_ng/filters.py similarity index 100% rename from cteward-ng/filters.py rename to cteward_ng/filters.py diff --git a/cteward-ng/mappings.py b/cteward_ng/mappings.py similarity index 100% rename from cteward-ng/mappings.py rename to cteward_ng/mappings.py diff --git a/cteward-ng/memberdata.py b/cteward_ng/memberdata.py similarity index 98% rename from cteward-ng/memberdata.py rename to cteward_ng/memberdata.py index 174a1f0..de6aa2f 100644 --- a/cteward-ng/memberdata.py +++ b/cteward_ng/memberdata.py @@ -64,7 +64,7 @@ def datum(isodate): return '1.1.1970' try: dt = datetime.strptime(isodate, '%Y%m%d') - return f'{dt.day}.{dt.month + 1}.{dt.year}' + return f'{dt.day}.{dt.month}.{dt.year}' except ValueError: return '1.1.1970' diff --git a/cteward-ng/permissions.py b/cteward_ng/permissions.py similarity index 100% rename from cteward-ng/permissions.py rename to cteward_ng/permissions.py diff --git a/cteward-ng/pytest.ini b/cteward_ng/pytest.ini similarity index 64% rename from cteward-ng/pytest.ini rename to cteward_ng/pytest.ini index 75e851b..89c5260 100644 --- a/cteward-ng/pytest.ini +++ b/cteward_ng/pytest.ini @@ -1,5 +1,3 @@ -"""pytest configuration.""" - [pytest] testpaths = cteward-ng/tests pythonpath = . diff --git a/cteward-ng/renderers.py b/cteward_ng/renderers.py similarity index 100% rename from cteward-ng/renderers.py rename to cteward_ng/renderers.py diff --git a/cteward-ng/requirements.txt b/cteward_ng/requirements.txt similarity index 100% rename from cteward-ng/requirements.txt rename to cteward_ng/requirements.txt diff --git a/cteward-ng/tests/__init__.py b/cteward_ng/tests/__init__.py similarity index 100% rename from cteward-ng/tests/__init__.py rename to cteward_ng/tests/__init__.py diff --git a/cteward_ng/tests/__pycache__/__init__.cpython-313.pyc b/cteward_ng/tests/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..997141eba4d7fa18c2fdf75060426dd0e1d50a22 GIT binary patch literal 148 zcmey&%ge<81p1eiv&4b)V-N=h7@>^MEI`IohI9r^M!%H|MNB~6XOPq_WBrW$+*JMI z+{~O*{p6C=^2DMP-Mn;UW=U#sNwI!>d}dx|NqoFsLFFwDo80`A(wtPgB37UoAUldd OjE~HWjEqIhKo$Tgts<8I literal 0 HcmV?d00001 diff --git a/cteward_ng/tests/__pycache__/conftest.cpython-313-pytest-9.0.3.pyc b/cteward_ng/tests/__pycache__/conftest.cpython-313-pytest-9.0.3.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a1e6a0a3035d152c3b7a94f46ec5876a508abe29 GIT binary patch literal 1029 zcmZ`%&1(}u6rb7M=3~>OiuKS^%lL)lkWE4S0Yeq){RFXx6uPsoEun`xH_Q;)(ww}y||U0aQ*gG6Hef_ z+_p7X6td*8gMG?bN%aBP10PW4k_0=XveSstjW_kiyRElHjVp*zDv6%pNQX9rf=Z&t zS6V3|IApr$Q6dwF=QcU#t#^Zm6$tK;25~5U5|oKt?9P_p74M))F>U#Wj@Ru z#@nTPiXhHD_7q4)+PTb1SFD z3Ri6v^HJKin}K87QD&#-deZepH2*s6+cRs)4*M}DR0zhU8~7Gy`y75+l%2Vv^wej< zqxsXQrf66^K1A{?7!4Ni{JSNyh;7p literal 0 HcmV?d00001 diff --git a/cteward_ng/tests/__pycache__/test_config.cpython-313-pytest-9.0.3.pyc b/cteward_ng/tests/__pycache__/test_config.cpython-313-pytest-9.0.3.pyc new file mode 100644 index 0000000000000000000000000000000000000000..adb54eb0238fc3fcf2f18b761a315ce62bedf4a0 GIT binary patch literal 8716 zcmeHM&2JmW6`x&l$t6XKl5I)08%rxWwnf*b6^oMFG)fcLu^pvg0InGuV1dO-TvAM_ zU3zwD*)G}!Xi+#tfg}gR%*{O%wQv0odg-Z$LSKN4jSCdWDL@aYjZ}b7eQ)-Y3Z>YF zUG$*Q>dnW#c{B64^WJaXJWi*R1d{cy!ykSWBjii0cq!@@9?wDHHjxP>GAHxzag=9y zWHxH@5}6ffOmg1DX`HzPf_ltlKEF(O9~5?PqyoiS*-#%E(U ze|bqWEot7+tMyuos#e|5rG}wa>Uu4gOn#uzdbO^ZlC_|ET)E~A#(|BRG%LSfaJx&Y zUXj#hvw~Dv{Y)fnz;{w!O+ZH@++K7A-&YwB zG4BaPxpDZ9!2jbsXyrC3V;f6FS4&6C7IH+6{xHH5ohy647AeB-xHSQ{AjjnR z1UJEzI8W;mDRITbR$fA;+$M>Xd%U;6nexg(KIP6|=gYlbYq8HO1^F0`v_pURe%N0y zv&+0J2p~X?A=M*t+&Ei`zJ8GqqcrXct{8-r_jsce_j#othjoi3A%LhEUAz25#wjhU5RLDb37CSj=b5 z`@9{2HYE8cz0ggg7JE0HHL-Cz+iQ!Kw%DxJ8=AeR06VO5391HF>5cQ)7R4#q(MoHv zX$#dx%Uq!NsoPN$DEk(QDY^XXjAF%JDrq-z1e09N&IXl|!fgu!U6%*mg zWVmwLsW>91!j&_o2ve8bWKW-3Fc!5_<|3@~)V!rFskCxZuX#lUintz=QP-$|*|PVs zRidRa71WNxly~@2hn2{6W5Nw){sqWKcXg_gNM5%bDX&bv;yGLvEkT zF96GCKZyQ@0C{4fMY!|!=($AXuZiTjp?G(5SOvrQqRrt17^J}1{QsIkSuiFq^FIPT z8Rz`X|Hr2=d$Cd!%wGJ;>E|(fvRK{^L$XxtYOFjLfitUouVX&1xE(_o7&qQCYThbT zFufs@*xeBOz24_!QBJrfR*;i&Pf2i1Y^*%u4Hc3nd2K-svxs@K@N&n*HuG}Im@Mt; znY?s8r+4*?U*t>IbFXn)P9t-zl=HmvIm|4n91dK4LWx)(CUf|DHpamoRRQvgCWdQGEtRBc%cc683L%rMq;wZ(b9n2N@7O+MZs?jBbeTvnr0> zy6)80gQEW$+S=28ZKb2;H?N7K4>AMG;z)aNB{Q-iy3eXOa_gE?TMvr3?#4bJUKWqFhgUMkHbnPX6_4QvSX&Q@YslvF_yuTwkQrPS$L|h)o`bBN zTgi-Xi0-p0j-!gPwjLDMkj>}u3&62?05J4_!a({^VKnmh(PUv_8z5bT^Dh8tI6FE? z<~)d00;D>KRq|()FC&)${*C&oz?mDcl@9r5n0-y>42XZk=kX)&h$` zG4MbqU>@`|66QcBv4rPGI*sHEk{AERyeEJ5G86;y!m}ibTp1IR0(>L+` zkAaxD(H*brc-%4OaTn{R3E^ZGYyrP-wWw|e?)SBB?svy7=;h5}Fsa~+`(If;G_}@0 zeJjSy{H_ai-7xss$DpQdqTRD_N_p1s4g_rQY_lc7eS+)TSY#XH>06Q)wvb5Kk9&ix zE}qAe1d(mu&lLR_cOVu$&QATm9od#++k|v{Pg3+_+Tn5JI1A~B9U+~dPuJ*o5XqJk zW&bEv^n*KL`JQfkPaDYY8UhNuc!#GAdUc;sIHZ&CqvisOng<~rMDi?^19=GSx1=3I zI@^UB0*SkNei3@^-6o{7tLGQ`J@0sg=L;v?BYfstNBEn)KdgeuSdgQs!($H~pVC$)MXm)SpZ+nxoWAXcN`JS*vg~ebL#ZD>;Twt~u8kAFt za^erT3kz52~4=4&;@muwIMYSwipKAdC1lCz( zcn1jvVkqJWM(}WFqL2J7dBi7#sYiopA^T`JDoEd?dIZVog4}S~8CR5wF%J_Fy`U2N z*zE&~b9tF_ZWrNdGDkJGY3zjGWXIh$CrZVlSuApe5iUFCNU;0et32Na72U`I4mnL9&~%6P)yj I0AXwUFMywmLjV8( literal 0 HcmV?d00001 diff --git a/cteward_ng/tests/__pycache__/test_memberdata.cpython-313-pytest-9.0.3.pyc b/cteward_ng/tests/__pycache__/test_memberdata.cpython-313-pytest-9.0.3.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c696c0018df7d31e18a0e834d5b63cf0106d4944 GIT binary patch literal 17956 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 literal 0 HcmV?d00001 diff --git a/cteward-ng/tests/conftest.py b/cteward_ng/tests/conftest.py similarity index 100% rename from cteward-ng/tests/conftest.py rename to cteward_ng/tests/conftest.py diff --git a/cteward-ng/tests/test_config.py b/cteward_ng/tests/test_config.py similarity index 82% rename from cteward-ng/tests/test_config.py rename to cteward_ng/tests/test_config.py index a0326e7..e1d1714 100644 --- a/cteward-ng/tests/test_config.py +++ b/cteward_ng/tests/test_config.py @@ -39,11 +39,13 @@ class TestLoadConfig: assert 'bots' in config['auth'] assert 'flags' in config['auth'] - def test_missing_file_returns_empty(self): + def test_missing_file_returns_defaults(self): config = load_config('/nonexistent/path.json') - assert config == {} + assert 'mssql' in config + assert 'server' in config + assert 'auth' in config - def test_invalid_json_returns_empty(self): + def test_invalid_json_returns_defaults(self): with tempfile.NamedTemporaryFile( mode='w', suffix='.json', delete=False ) as fh: @@ -52,4 +54,6 @@ class TestLoadConfig: config = load_config(fh.name) os.unlink(fh.name) - assert config == {} + assert 'mssql' in config + assert 'server' in config + assert 'auth' in config diff --git a/cteward-ng/tests/test_memberdata.py b/cteward_ng/tests/test_memberdata.py similarity index 97% rename from cteward-ng/tests/test_memberdata.py rename to cteward_ng/tests/test_memberdata.py index 5f67cd7..0a5e772 100644 --- a/cteward-ng/tests/test_memberdata.py +++ b/cteward_ng/tests/test_memberdata.py @@ -16,7 +16,7 @@ from cteward_ng.memberdata import ( class TestDatum: def test_valid_yyyy_mmdd(self): - assert datum('20230115') == '15.2.2023' + assert datum('20230115') == '15.1.2023' def test_invalid_length(self): assert datum('2023011') == '1.1.1970' diff --git a/cteward-ng/views.py b/cteward_ng/views.py similarity index 100% rename from cteward-ng/views.py rename to cteward_ng/views.py diff --git a/podman-compose.yml b/podman-compose.yml index 656ab01..0120881 100644 --- a/podman-compose.yml +++ b/podman-compose.yml @@ -1,5 +1,11 @@ services: - cteward: - build: . - ports: - - "${APP_PORT}:5000" + cteward: + build: . + ports: + - "${APP_PORT:-5000}:5000" + environment: + - CTEWARD_ST_LEXWARE_CONFIG=/etc/cteward/st-lexware.json + volumes: + - /etc/cteward:/etc/cteward:ro + - /var/log/cteward:/var/log/cteward + restart: unless-stopped