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 diff --git a/.gitea/workflows/test_pr.yml b/.gitea/workflows/test_pr.yml new file mode 100644 index 0000000..cfd1a1f --- /dev/null +++ b/.gitea/workflows/test_pr.yml @@ -0,0 +1,77 @@ +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: 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 . + + - 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 + + - name: Start server in container and smoke test HTTP (in-container) + run: | + set -x + # start the server inside the container (detached) + docker exec -d semapform-test python api_service.py || true + + # show container status to aid debugging + docker ps -a --filter name=semapform-test || true + + # 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 + docker logs semapform-test --tail 200 || true + + - name: Cleanup container + if: always() + run: | + docker rm -f semapform-test || true 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 . . diff --git a/api_service.py b/api_service.py index 3f36b08..6c2662f 100644 --- a/api_service.py +++ b/api_service.py @@ -1,12 +1,11 @@ -""" -Lightweight Python API service for signature validation +"""Lightweight Python API service for signature validation 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 @@ -22,15 +21,25 @@ 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: + # import inside function to avoid expensive work during module import + from bibapi import catalogue as _catalogue + + 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 +48,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 +56,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, diff --git a/pyproject.toml b/pyproject.toml index 4547ffe..6afc03e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,11 @@ version = "0.1.1" 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"))