Compare commits
23 Commits
v0.1.1
...
5a1c19ccbe
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
36a548eee6 | ||
| 5a1c19ccbe | |||
|
36f5dd14b5
|
|||
|
811313d1ef
|
|||
|
c44dc5a61c
|
|||
|
c9f139d29c
|
|||
|
6ef463458f
|
|||
|
7a74ead335
|
|||
|
6f38dd482e
|
|||
|
e1da6085ed
|
|||
|
af1ee0ce71
|
|||
|
0e7c45182a
|
|||
|
c21afa0776
|
|||
|
df527b30da
|
|||
|
0cf88113be
|
|||
| 59c90ca471 | |||
|
0d9ffb8539
|
|||
| 1e7d45ec49 | |||
|
2ee575aff4
|
|||
| 69e463c9a4 | |||
| b1cdeffb4c | |||
| e2cd6d7ba3 | |||
|
6837d0b4b3
|
@@ -13,3 +13,4 @@ dist/
|
|||||||
.gitignore
|
.gitignore
|
||||||
.env
|
.env
|
||||||
*.sock
|
*.sock
|
||||||
|
.venv
|
||||||
77
.gitea/workflows/test_pr.yml
Normal file
77
.gitea/workflows/test_pr.yml
Normal file
@@ -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
|
||||||
44
Dockerfile
44
Dockerfile
@@ -1,43 +1,13 @@
|
|||||||
FROM python:3.13-slim AS builder
|
FROM python:3.13.9-slim
|
||||||
|
|
||||||
# Prevent Python from writing .pyc files and enable unbuffered stdout/stderr
|
RUN apt update
|
||||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
RUN apt upgrade -y
|
||||||
PYTHONUNBUFFERED=1 \
|
|
||||||
PIP_DISABLE_PIP_VERSION_CHECK=1 \
|
|
||||||
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1
|
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Install build deps required to build wheels
|
COPY requirements.txt .
|
||||||
RUN apt-get update \
|
RUN pip install --no-cache-dir -r requirements.txt --extra-index-url https://git.theprivateserver.de/api/packages/PHB/pypi/simple/
|
||||||
&& apt-get install -y --no-install-recommends build-essential gcc \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Upgrade packaging tools and install runtime dependencies into a separate prefix
|
COPY . .
|
||||||
COPY requirements.txt ./
|
|
||||||
RUN pip install --upgrade pip setuptools wheel \
|
|
||||||
&& pip install --prefix=/install --no-cache-dir \
|
|
||||||
--extra-index-url https://git.theprivateserver.de/api/packages/PHB/pypi/simple/ \
|
|
||||||
-r requirements.txt \
|
|
||||||
&& rm -rf /root/.cache/pip
|
|
||||||
|
|
||||||
# Copy application source (only used to include app files in final image)
|
ENTRYPOINT [ "python", "api_service.py" ]
|
||||||
COPY . /app
|
|
||||||
|
|
||||||
FROM python:3.13-slim
|
|
||||||
|
|
||||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
|
||||||
PYTHONUNBUFFERED=1 \
|
|
||||||
API_PORT=8001
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Copy installed packages from the builder image into the final image
|
|
||||||
COPY --from=builder /install /usr/local
|
|
||||||
# Copy application sources
|
|
||||||
COPY --from=builder /app /app
|
|
||||||
|
|
||||||
EXPOSE 8001
|
|
||||||
|
|
||||||
# Run using uvicorn; the app is defined in `api_service.py` as `app`
|
|
||||||
CMD ["uvicorn", "api_service:app", "--host", "0.0.0.0", "--port", "8001"]
|
|
||||||
@@ -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
|
This can run independently to support the PHP application
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import re
|
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 import FastAPI, Query
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
@@ -22,15 +21,25 @@ app.add_middleware(
|
|||||||
allow_headers=["*"],
|
allow_headers=["*"],
|
||||||
)
|
)
|
||||||
|
|
||||||
# Initialize catalogue for signature validation
|
# Catalogue is expensive to initialize at import time; instantiate lazily
|
||||||
cat = catalogue.Catalogue()
|
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")
|
@app.get("/api/validate-signature")
|
||||||
async def validate_signature(signature: str = Query(...)):
|
async def validate_signature(signature: str = Query(...)):
|
||||||
"""Validate a book signature and return total pages"""
|
"""Validate a book signature and return total pages"""
|
||||||
try:
|
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:
|
if book_result and hasattr(book_result, "pages") and book_result.pages:
|
||||||
# Try to extract numeric page count
|
# Try to extract numeric page count
|
||||||
pages_str = str(book_result.pages)
|
pages_str = str(book_result.pages)
|
||||||
@@ -39,7 +48,7 @@ async def validate_signature(signature: str = Query(...)):
|
|||||||
if match:
|
if match:
|
||||||
total_pages = int(match.group(1))
|
total_pages = int(match.group(1))
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
{"valid": True, "total_pages": total_pages, "signature": signature}
|
{"valid": True, "total_pages": total_pages, "signature": signature},
|
||||||
)
|
)
|
||||||
|
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
@@ -47,13 +56,13 @@ async def validate_signature(signature: str = Query(...)):
|
|||||||
"valid": False,
|
"valid": False,
|
||||||
"error": "Signatur nicht gefunden oder keine Seitenzahl verfügbar",
|
"error": "Signatur nicht gefunden oder keine Seitenzahl verfügbar",
|
||||||
"signature": signature,
|
"signature": signature,
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
{
|
{
|
||||||
"valid": False,
|
"valid": False,
|
||||||
"error": f"Fehler bei der Validierung: {str(e)}",
|
"error": f"Fehler bei der Validierung: {e!s}",
|
||||||
"signature": signature,
|
"signature": signature,
|
||||||
},
|
},
|
||||||
status_code=500,
|
status_code=500,
|
||||||
|
|||||||
@@ -1,20 +1,21 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "semapform-api"
|
name = "semapform-api"
|
||||||
version = "0.1.1"
|
version = "0.1.2"
|
||||||
description = "Add your description here"
|
description = "Add your description here"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.13"
|
requires-python = ">=3.13"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bibapi[catalogue]>=0.0.5",
|
"bibapi>=0.0.5",
|
||||||
"fastapi>=0.122.0",
|
"fastapi>=0.122.0",
|
||||||
"pip>=25.3",
|
"pip>=25.3",
|
||||||
|
"uvicorn>=0.38.0",
|
||||||
]
|
]
|
||||||
[[tool.uv.index]]
|
[[tool.uv.index]]
|
||||||
name = "gitea"
|
name = "gitea"
|
||||||
url = "https://git.theprivateserver.de/api/packages/PHB/pypi/simple/"
|
url = "https://git.theprivateserver.de/api/packages/PHB/pypi/simple/"
|
||||||
|
|
||||||
[tool.bumpversion]
|
[tool.bumpversion]
|
||||||
current_version = "0.1.1"
|
current_version = "0.1.2"
|
||||||
parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)"
|
parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)"
|
||||||
serialize = ["{major}.{minor}.{patch}"]
|
serialize = ["{major}.{minor}.{patch}"]
|
||||||
search = "{current_version}"
|
search = "{current_version}"
|
||||||
|
|||||||
8
test_api.py
Normal file
8
test_api.py
Normal file
@@ -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"))
|
||||||
Reference in New Issue
Block a user