From 0cf88113be6e7e0b6888501e0660b230faeb1797 Mon Sep 17 00:00:00 2001 From: WorldTeacher Date: Tue, 25 Nov 2025 09:03:29 +0100 Subject: [PATCH 01/13] maintenance: add .venv to dockerignore --- .dockerignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.dockerignore b/.dockerignore index e81bc25..e2027ed 100644 --- a/.dockerignore +++ b/.dockerignore @@ -13,3 +13,4 @@ dist/ .gitignore .env *.sock +.venv \ No newline at end of file From df527b30dab53c4fbe226a7727930194ce11ae04 Mon Sep 17 00:00:00 2001 From: WorldTeacher Date: Tue, 25 Nov 2025 09:09:44 +0100 Subject: [PATCH 02/13] feat: add pr test workflow --- .gitea/workflows/test_pr.yml | 52 ++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 .gitea/workflows/test_pr.yml diff --git a/.gitea/workflows/test_pr.yml b/.gitea/workflows/test_pr.yml new file mode 100644 index 0000000..979176d --- /dev/null +++ b/.gitea/workflows/test_pr.yml @@ -0,0 +1,52 @@ +name: PR tests + +on: + pull_request: + types: [opened, synchronize, edited, reopened] + +jobs: + build-and-smoke: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Build image + run: | + docker build -t semapform-api:test-pr . + + - name: Start container (background) + run: | + docker run -d --name semapform-test -p 8001:8001 semapform-api:test-pr sleep infinity + + - name: Verify python module imports + run: | + docker exec semapform-test python -c "import api_service; print('import ok')" + + - name: Start server in container and smoke test HTTP + env: + PORT: 8001 + run: | + # start uvicorn inside the container (background) + docker exec -d semapform-test uv run uvicorn api_service:app --host 0.0.0.0 --port $PORT + + # wait for up to 20s for the server to respond + SECONDS=0 + until curl -sS "http://localhost:${PORT}/" -o /dev/null; do + sleep 1 + SECONDS=$((SECONDS+1)) + if [ $SECONDS -ge 20 ]; then + echo "server failed to respond within timeout" >&2 + docker logs semapform-test || true + exit 1 + fi + done + + - name: Cleanup container + if: always() + run: | + docker rm -f semapform-test || true From c21afa0776583f69d48f9923711b17a1efc172b7 Mon Sep 17 00:00:00 2001 From: WorldTeacher Date: Tue, 25 Nov 2025 09:10:03 +0100 Subject: [PATCH 03/13] chore: add custom pypi url --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 1010f84..85ae883 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,7 +6,7 @@ RUN apt upgrade -y WORKDIR /app COPY requirements.txt . -RUN pip install --no-cache-dir -r requirements.txt +RUN pip install --no-cache-dir -r requirements.txt --extra-index-url https://git.theprivateserver.de/api/packages/PHB/pypi/simple/ COPY . . From 0e7c45182a479c87592f9a44333e30e8e9645314 Mon Sep 17 00:00:00 2001 From: WorldTeacher Date: Tue, 25 Nov 2025 09:12:51 +0100 Subject: [PATCH 04/13] chore: add uv, generate requirements.txt for tests --- .gitea/workflows/test_pr.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.gitea/workflows/test_pr.yml b/.gitea/workflows/test_pr.yml index 979176d..4be7bc2 100644 --- a/.gitea/workflows/test_pr.yml +++ b/.gitea/workflows/test_pr.yml @@ -15,6 +15,19 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v2 + - name: Install uv + uses: astral-sh/setup-uv@v5 + - name: Set up Python + run: uv python install + with: + python-version-file: "pyproject.toml" + - name: Install the project dependencies + run: | + uv sync --all-groups + uv add pip + uv export --format requirements.txt -o requirements.txt + # uv run python -m pip install --upgrade pip + # uv run python -m pip install -r requirements.txt - name: Build image run: | docker build -t semapform-api:test-pr . From af1ee0ce7173b6506c767841179812d665dbbe26 Mon Sep 17 00:00:00 2001 From: WorldTeacher Date: Tue, 25 Nov 2025 09:15:54 +0100 Subject: [PATCH 05/13] test: lazy initiation of catalogue --- api_service.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/api_service.py b/api_service.py index 3f36b08..c074c57 100644 --- a/api_service.py +++ b/api_service.py @@ -1,5 +1,4 @@ -""" -Lightweight Python API service for signature validation +"""Lightweight Python API service for signature validation This can run independently to support the PHP application """ @@ -22,15 +21,22 @@ app.add_middleware( allow_headers=["*"], ) -# Initialize catalogue for signature validation -cat = catalogue.Catalogue() +# Catalogue is expensive to initialize at import time; instantiate lazily +cat = None + + +def _get_catalogue(): + global cat + if cat is None: + cat = catalogue.Catalogue() + return cat @app.get("/api/validate-signature") async def validate_signature(signature: str = Query(...)): """Validate a book signature and return total pages""" try: - book_result = cat.get_book_with_data(signature) + book_result = _get_catalogue().get_book_with_data(signature) if book_result and hasattr(book_result, "pages") and book_result.pages: # Try to extract numeric page count pages_str = str(book_result.pages) @@ -39,7 +45,7 @@ async def validate_signature(signature: str = Query(...)): if match: total_pages = int(match.group(1)) return JSONResponse( - {"valid": True, "total_pages": total_pages, "signature": signature} + {"valid": True, "total_pages": total_pages, "signature": signature}, ) return JSONResponse( @@ -47,13 +53,13 @@ async def validate_signature(signature: str = Query(...)): "valid": False, "error": "Signatur nicht gefunden oder keine Seitenzahl verfügbar", "signature": signature, - } + }, ) except Exception as e: return JSONResponse( { "valid": False, - "error": f"Fehler bei der Validierung: {str(e)}", + "error": f"Fehler bei der Validierung: {e!s}", "signature": signature, }, status_code=500, From e1da6085edbaec227783c035c19e4323674b7044 Mon Sep 17 00:00:00 2001 From: WorldTeacher Date: Tue, 25 Nov 2025 09:20:15 +0100 Subject: [PATCH 06/13] test new workflow --- .gitea/workflows/test_pr.yml | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/.gitea/workflows/test_pr.yml b/.gitea/workflows/test_pr.yml index 4be7bc2..0992795 100644 --- a/.gitea/workflows/test_pr.yml +++ b/.gitea/workflows/test_pr.yml @@ -34,30 +34,31 @@ jobs: - name: Start container (background) run: | - docker run -d --name semapform-test -p 8001:8001 semapform-api:test-pr sleep infinity + # do NOT bind the container port to the host to avoid port conflicts on the runner + docker run -d --name semapform-test semapform-api:test-pr sleep infinity - name: Verify python module imports run: | docker exec semapform-test python -c "import api_service; print('import ok')" - - name: Start server in container and smoke test HTTP - env: - PORT: 8001 + - name: Start server in container and smoke test HTTP (in-container) run: | - # start uvicorn inside the container (background) - docker exec -d semapform-test uv run uvicorn api_service:app --host 0.0.0.0 --port $PORT + # start the server inside the container + docker exec -d semapform-test uv run uvicorn api_service:app --host 0.0.0.0 --port 8001 - # wait for up to 20s for the server to respond - SECONDS=0 - until curl -sS "http://localhost:${PORT}/" -o /dev/null; do - sleep 1 - SECONDS=$((SECONDS+1)) - if [ $SECONDS -ge 20 ]; then - echo "server failed to respond within timeout" >&2 - docker logs semapform-test || true - exit 1 - fi - done + # perform an in-container HTTP check using Python stdlib (avoids requiring curl) + docker exec semapform-test python - << 'PY' + import time,urllib.request,sys + for _ in range(20): + try: + urllib.request.urlopen('http://127.0.0.1:8001/health', timeout=2) + print('ok') + sys.exit(0) + except Exception: + time.sleep(1) + print('failed') + sys.exit(1) + PY - name: Cleanup container if: always() From 6f38dd482e3abb3171528943588b6536cdefee6f Mon Sep 17 00:00:00 2001 From: WorldTeacher Date: Tue, 25 Nov 2025 09:23:29 +0100 Subject: [PATCH 07/13] chore: only import catalogue if needed --- api_service.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/api_service.py b/api_service.py index c074c57..ceca917 100644 --- a/api_service.py +++ b/api_service.py @@ -5,7 +5,7 @@ This can run independently to support the PHP application import os import re -from bibapi import catalogue +# Avoid importing heavy modules at top-level to keep `import api_service` lightweight from fastapi import FastAPI, Query from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse @@ -28,7 +28,9 @@ cat = None def _get_catalogue(): global cat if cat is None: - cat = catalogue.Catalogue() + # import inside function to avoid expensive work during module import + from bibapi import catalogue as _catalogue + cat = _catalogue.Catalogue() return cat From 7a74ead3352c53a53e12e04314bc3aa4cc7fd9c5 Mon Sep 17 00:00:00 2001 From: WorldTeacher Date: Tue, 25 Nov 2025 09:41:11 +0100 Subject: [PATCH 08/13] chore: new service check using signature --- .gitea/workflows/test_pr.yml | 23 +++++++++++++++++++++-- api_service.py | 1 + 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/.gitea/workflows/test_pr.yml b/.gitea/workflows/test_pr.yml index 0992795..c277fa5 100644 --- a/.gitea/workflows/test_pr.yml +++ b/.gitea/workflows/test_pr.yml @@ -37,9 +37,28 @@ jobs: # do NOT bind the container port to the host to avoid port conflicts on the runner docker run -d --name semapform-test semapform-api:test-pr sleep infinity - - name: Verify python module imports + - name: Start server in container and smoke test HTTP (in-container) run: | - docker exec semapform-test python -c "import api_service; print('import ok')" + # start the server inside the container + docker exec -d semapform-test uv run uvicorn api_service:app --host 0.0.0.0 --port 8001 + + # send a POST request to /api/validate-signature with signature="ST 250 U42 (15)" + docker exec semapform-test python - << 'PY' + import time, urllib.request, sys + url = 'http://127.0.0.1:8001/api/validate-signature?signature=ST%20250%20U42%20%2815%29' + for _ in range(20): + try: + req = urllib.request.Request(url, method='POST') + r = urllib.request.urlopen(req, timeout=3) + print('status', r.status) + print(r.read().decode()) + if 200 <= r.status < 300: + sys.exit(0) + except Exception: + time.sleep(1) + print('failed') + sys.exit(1) + PY - name: Start server in container and smoke test HTTP (in-container) run: | diff --git a/api_service.py b/api_service.py index ceca917..6c2662f 100644 --- a/api_service.py +++ b/api_service.py @@ -30,6 +30,7 @@ def _get_catalogue(): if cat is None: # import inside function to avoid expensive work during module import from bibapi import catalogue as _catalogue + cat = _catalogue.Catalogue() return cat From 6ef463458f12c7438145f40da81daf0bcdf19942 Mon Sep 17 00:00:00 2001 From: WorldTeacher Date: Tue, 25 Nov 2025 09:46:46 +0100 Subject: [PATCH 09/13] use python instead of uv --- .gitea/workflows/test_pr.yml | 21 +-------------------- 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/.gitea/workflows/test_pr.yml b/.gitea/workflows/test_pr.yml index c277fa5..bc1a288 100644 --- a/.gitea/workflows/test_pr.yml +++ b/.gitea/workflows/test_pr.yml @@ -40,7 +40,7 @@ jobs: - name: Start server in container and smoke test HTTP (in-container) run: | # start the server inside the container - docker exec -d semapform-test uv run uvicorn api_service:app --host 0.0.0.0 --port 8001 + docker exec -d semapform-test python api_service.py # send a POST request to /api/validate-signature with signature="ST 250 U42 (15)" docker exec semapform-test python - << 'PY' @@ -60,25 +60,6 @@ jobs: sys.exit(1) PY - - name: Start server in container and smoke test HTTP (in-container) - run: | - # start the server inside the container - docker exec -d semapform-test uv run uvicorn api_service:app --host 0.0.0.0 --port 8001 - - # perform an in-container HTTP check using Python stdlib (avoids requiring curl) - docker exec semapform-test python - << 'PY' - import time,urllib.request,sys - for _ in range(20): - try: - urllib.request.urlopen('http://127.0.0.1:8001/health', timeout=2) - print('ok') - sys.exit(0) - except Exception: - time.sleep(1) - print('failed') - sys.exit(1) - PY - - name: Cleanup container if: always() run: | From c9f139d29c8e20a00c8f8f3ff6516111485d9b98 Mon Sep 17 00:00:00 2001 From: WorldTeacher Date: Tue, 25 Nov 2025 09:49:07 +0100 Subject: [PATCH 10/13] re-add port declaration --- .gitea/workflows/test_pr.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitea/workflows/test_pr.yml b/.gitea/workflows/test_pr.yml index bc1a288..19874b3 100644 --- a/.gitea/workflows/test_pr.yml +++ b/.gitea/workflows/test_pr.yml @@ -35,7 +35,7 @@ jobs: - name: Start container (background) run: | # do NOT bind the container port to the host to avoid port conflicts on the runner - docker run -d --name semapform-test semapform-api:test-pr sleep infinity + docker run -d --name semapform-test semapform-api:test-pr --port 8001:8001 sleep infinity - name: Start server in container and smoke test HTTP (in-container) run: | From c44dc5a61cb9033b78bf34195fa8fc0b4d0e26bf Mon Sep 17 00:00:00 2001 From: WorldTeacher Date: Tue, 25 Nov 2025 09:52:51 +0100 Subject: [PATCH 11/13] test new validation test --- .gitea/workflows/test_pr.yml | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/.gitea/workflows/test_pr.yml b/.gitea/workflows/test_pr.yml index 19874b3..a762f0b 100644 --- a/.gitea/workflows/test_pr.yml +++ b/.gitea/workflows/test_pr.yml @@ -35,31 +35,38 @@ jobs: - name: Start container (background) run: | # do NOT bind the container port to the host to avoid port conflicts on the runner - docker run -d --name semapform-test semapform-api:test-pr --port 8001:8001 sleep infinity + docker run -d --name semapform-test semapform-api:test-pr sleep infinity - name: Start server in container and smoke test HTTP (in-container) run: | - # start the server inside the container - docker exec -d semapform-test python api_service.py + set -x + # start the server inside the container (detached) + docker exec -d semapform-test python api_service.py || true - # send a POST request to /api/validate-signature with signature="ST 250 U42 (15)" + # show container status to aid debugging + docker ps -a --filter name=semapform-test || true + + # perform an in-container GET request (endpoint is a GET) and print attempts/logs docker exec semapform-test python - << 'PY' import time, urllib.request, sys url = 'http://127.0.0.1:8001/api/validate-signature?signature=ST%20250%20U42%20%2815%29' - for _ in range(20): + for i in range(20): try: - req = urllib.request.Request(url, method='POST') - r = urllib.request.urlopen(req, timeout=3) - print('status', r.status) - print(r.read().decode()) - if 200 <= r.status < 300: - sys.exit(0) - except Exception: + with urllib.request.urlopen(url, timeout=3) as r: + print('attempt', i, 'status', r.status) + print(r.read().decode()) + if 200 <= r.status < 300: + sys.exit(0) + except Exception as e: + print('attempt', i, 'failed:', e) time.sleep(1) print('failed') sys.exit(1) PY + # dump the last 200 lines of logs so this step has visible output + docker logs semapform-test --tail 200 || true + - name: Cleanup container if: always() run: | From 811313d1ef7bdcf51d3885116d6e641e25e75afd Mon Sep 17 00:00:00 2001 From: WorldTeacher Date: Tue, 25 Nov 2025 09:59:24 +0100 Subject: [PATCH 12/13] fix: add uvicorn dependency feat: add test_api file --- .gitea/workflows/test_pr.yml | 34 ++++++++++++++++++---------------- pyproject.toml | 6 +++++- test_api.py | 8 ++++++++ 3 files changed, 31 insertions(+), 17 deletions(-) create mode 100644 test_api.py diff --git a/.gitea/workflows/test_pr.yml b/.gitea/workflows/test_pr.yml index a762f0b..ee459ef 100644 --- a/.gitea/workflows/test_pr.yml +++ b/.gitea/workflows/test_pr.yml @@ -46,26 +46,28 @@ jobs: # show container status to aid debugging docker ps -a --filter name=semapform-test || true - # perform an in-container GET request (endpoint is a GET) and print attempts/logs - docker exec semapform-test python - << 'PY' + # perform a readiness loop and then run the repo test script to exercise the API + docker exec semapform-test python - << 'PY' import time, urllib.request, sys - url = 'http://127.0.0.1:8001/api/validate-signature?signature=ST%20250%20U42%20%2815%29' + url = 'http://127.0.0.1:8001/health' for i in range(20): - try: - with urllib.request.urlopen(url, timeout=3) as r: - print('attempt', i, 'status', r.status) - print(r.read().decode()) - if 200 <= r.status < 300: - sys.exit(0) - except Exception as e: - print('attempt', i, 'failed:', e) - time.sleep(1) - print('failed') + try: + with urllib.request.urlopen(url, timeout=2) as r: + print('ready', i, 'status', r.status) + if 200 <= r.status < 300: + sys.exit(0) + except Exception as e: + print('ready attempt', i, 'failed:', e) + time.sleep(1) + print('service did not become ready') sys.exit(1) - PY + PY - # dump the last 200 lines of logs so this step has visible output - docker logs semapform-test --tail 200 || true + # Run the repository smoke-test script inside the container and surface its output + docker exec semapform-test python test_api.py || true + + # dump the last 200 lines of logs so this step always displays useful output + docker logs semapform-test --tail 200 || true - name: Cleanup container if: always() diff --git a/pyproject.toml b/pyproject.toml index 04d13ea..5bbe282 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,11 @@ version = "0.1.0" description = "Add your description here" readme = "README.md" requires-python = ">=3.13" -dependencies = ["bibapi>=0.0.5", "fastapi>=0.122.0"] +dependencies = [ + "bibapi>=0.0.5", + "fastapi>=0.122.0", + "uvicorn>=0.38.0", +] [[tool.uv.index]] name = "gitea" url = "https://git.theprivateserver.de/api/packages/PHB/pypi/simple/" diff --git a/test_api.py b/test_api.py new file mode 100644 index 0000000..bec1e82 --- /dev/null +++ b/test_api.py @@ -0,0 +1,8 @@ +# test api endpoint with a signature, print the response +import urllib.request + +response = urllib.request.urlopen( + "http://localhost:8001/api/validate-signature?signature=ST%20250%20U42%20%2815%29", +) + +print(response.read().decode("utf-8")) From 36f5dd14b5b21d64da11de50bb4712e4f4bbc75c Mon Sep 17 00:00:00 2001 From: WorldTeacher Date: Tue, 25 Nov 2025 10:03:13 +0100 Subject: [PATCH 13/13] check --- .gitea/workflows/test_pr.yml | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/.gitea/workflows/test_pr.yml b/.gitea/workflows/test_pr.yml index ee459ef..cfd1a1f 100644 --- a/.gitea/workflows/test_pr.yml +++ b/.gitea/workflows/test_pr.yml @@ -46,24 +46,26 @@ jobs: # show container status to aid debugging docker ps -a --filter name=semapform-test || true - # perform a readiness loop and then run the repo test script to exercise the API - docker exec semapform-test python - << 'PY' - import time, urllib.request, sys - url = 'http://127.0.0.1:8001/health' - for i in range(20): - try: - with urllib.request.urlopen(url, timeout=2) as r: - print('ready', i, 'status', r.status) - if 200 <= r.status < 300: - sys.exit(0) - except Exception as e: - print('ready attempt', i, 'failed:', e) - time.sleep(1) - print('service did not become ready') - sys.exit(1) - PY + # perform a readiness loop (try container-local /health) using small execs + echo "waiting for service to become ready inside container" + set -e + READY=0 + for i in $(seq 1 20); do + echo "ready attempt $i" + if docker exec semapform-test python -c 'import urllib.request,sys; urllib.request.urlopen("http://127.0.0.1:8001/health", timeout=1); print("ok")' ; then + READY=1 + break + fi + sleep 1 + done + if [ "$READY" -ne 1 ]; then + echo "service did not become ready" + docker logs semapform-test --tail 200 || true + exit 1 + fi # Run the repository smoke-test script inside the container and surface its output + echo "running test_api.py inside container" docker exec semapform-test python test_api.py || true # dump the last 200 lines of logs so this step always displays useful output