build: upgrade to Python 3.14 on Debian Trixie

- Bump requires-python to >=3.14 and ruff target-version to py314
- Switch all service images to uv:python3.14-trixie-slim builder
  and python:3.14-slim-trixie runtime
- Rename libmagic1 -> libmagic1t64 in worker-extract for Trixie
- Regenerate uv.lock under Python 3.14
- Update ARCHITECTURE.md to reflect py3.14 + Trixie
This commit is contained in:
febux 2026-09-06 15:29:49 +03:00
parent 2d5c2b50c0
commit f24117c2d1
14 changed files with 465 additions and 780 deletions

View file

@ -1 +1 @@
3.13 3.14

View file

@ -136,7 +136,7 @@ Control plane:
| Deploy | Compose now, k8s-ready later | — | | Deploy | Compose now, k8s-ready later | — |
| Prototype | Removed (stage-0 standalone benchmark no longer needed) | Kept as standalone benchmark | | Prototype | Removed (stage-0 standalone benchmark no longer needed) | Kept as standalone benchmark |
| Tests | pytest+respx unit + testcontainers integration | — | | Tests | pytest+respx unit + testcontainers integration | — |
| Python | **3.13** (was 3.14) — wheel availability | py3.14 | | Python | **3.14** | py3.13 |
| Landing | Incremental, green per step | — | | Landing | Incremental, green per step | — |
--- ---
@ -1184,14 +1184,14 @@ Same Ollama env as above; no DB/MQ/S3 env needed.
## 12. Docker — images & compose ## 12. Docker — images & compose
### Per-service Dockerfile pattern (uv, multi-stage, py3.13) ### Per-service Dockerfile pattern (uv, multi-stage, py3.14)
Dockerfiles live in `srv/<service>/Dockerfile` (one per service). Common shape Dockerfiles live in `srv/<service>/Dockerfile` (one per service). Common shape
(shown for api): (shown for api):
```dockerfile ```dockerfile
# syntax=docker/dockerfile:1 # syntax=docker/dockerfile:1
FROM ghcr.io/astral-sh/uv:python3.13-bookworm-slim AS builder FROM ghcr.io/astral-sh/uv:python3.14-trixie-slim AS builder
ENV UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy UV_PYTHON_DOWNLOADS=never \ ENV UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy UV_PYTHON_DOWNLOADS=never \
UV_PROJECT_ENVIRONMENT=/app/.venv UV_PROJECT_ENVIRONMENT=/app/.venv
WORKDIR /app WORKDIR /app
@ -1201,7 +1201,7 @@ COPY src ./src
RUN --mount=type=cache,target=/root/.cache/uv \ RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --frozen --no-default-groups --group api uv sync --frozen --no-default-groups --group api
FROM python:3.13-slim AS runtime FROM python:3.14-slim-trixie AS runtime
ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 PATH=/app/.venv/bin:$PATH ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 PATH=/app/.venv/bin:$PATH
WORKDIR /app WORKDIR /app
COPY --from=builder /app/.venv /app/.venv COPY --from=builder /app/.venv /app/.venv
@ -1218,14 +1218,14 @@ Per-service differences (`uv` uses PEP 735 dependency-groups — see `pyproject.
| Dockerfile | Extra installed | Runtime apt | CMD | Expose | | Dockerfile | Extra installed | Runtime apt | CMD | Expose |
|---|---|---|---|---| |---|---|---|---|---|
| `srv/api/Dockerfile` | `--group api` | none | `python -m contract_check.api` | 8000, 9100 | | `srv/api/Dockerfile` | `--group api` | none | `python -m contract_check.api` | 8000, 9100 |
| `srv/worker-extract/Dockerfile` | `--group extract` | tesseract-ocr, -rus, -eng, libmagic1 | `python -m contract_check.worker_extract` | 9101 | | `srv/worker-extract/Dockerfile` | `--group extract` | tesseract-ocr, -rus, -eng, libmagic1t64 | `python -m contract_check.worker_extract` | 9101 |
| `srv/worker-prescreen/Dockerfile` | `--group prescreen` | none | `python -m contract_check.worker_prescreen` | 9104 | | `srv/worker-prescreen/Dockerfile` | `--group prescreen` | none | `python -m contract_check.worker_prescreen` | 9104 |
| `srv/worker-analyze/Dockerfile` | `--group analyze` | none | `python -m contract_check.worker_analyze` | 9102 | | `srv/worker-analyze/Dockerfile` | `--group analyze` | none | `python -m contract_check.worker_analyze` | 9102 |
| `srv/bot/Dockerfile` | `--group bot` | none | `python -m contract_check.bot` | — | | `srv/bot/Dockerfile` | `--group bot` | none | `python -m contract_check.bot` | — |
The bot image is the leanest (no DB driver, no S3 client, no pymupdf). The The bot image is the leanest (no DB driver, no S3 client, no pymupdf). The
analyze image has httpx but no tesseract/pymupdf. The extract image is the analyze image has httpx but no tesseract/pymupdf. The extract image is the
heaviest (tesseract + language packs + libmagic1). This is the "fine-tuned deps heaviest (tesseract + language packs + libmagic1t64). This is the "fine-tuned deps
per service" payoff. per service" payoff.
### pyproject.toml dependency-groups (actual — PEP 735) ### pyproject.toml dependency-groups (actual — PEP 735)
@ -1239,7 +1239,7 @@ that need them. Sketch (see `pyproject.toml` for the authoritative list):
```toml ```toml
[project] [project]
name = "contract-check" name = "contract-check"
requires-python = ">=3.13" requires-python = ">=3.14"
dependencies = [ dependencies = [
"pydantic>=2.7", "pydantic-settings>=2.3", "structlog>=24.1", "pydantic>=2.7", "pydantic-settings>=2.3", "structlog>=24.1",
"python-dotenv>=1.0", "httpx[http2]>=0.27", "python-dotenv>=1.0", "httpx[http2]>=0.27",

View file

@ -7,7 +7,7 @@ name = "contract-check"
version = "0.1.0" version = "0.1.0"
description = "AI-скрининг рисков в договорах (PDF/DOCX) для СНГ — ГК РФ / ГК РБ. Event-driven production app." description = "AI-скрининг рисков в договорах (PDF/DOCX) для СНГ — ГК РФ / ГК РБ. Event-driven production app."
readme = "README.md" readme = "README.md"
requires-python = ">=3.13" requires-python = ">=3.14"
license = { text = "Proprietary" } license = { text = "Proprietary" }
authors = [{ name = "Контракт-чек" }] authors = [{ name = "Контракт-чек" }]
keywords = ["legal", "contracts", "llm", "risk-screening", "fastapi", "rabbitmq"] keywords = ["legal", "contracts", "llm", "risk-screening", "fastapi", "rabbitmq"]
@ -142,7 +142,7 @@ dev = [
[tool.ruff] [tool.ruff]
line-length = 100 line-length = 100
target-version = "py313" target-version = "py314"
src = ["src", "tests"] src = ["src", "tests"]
# Alembic migrations are autogenerated-style history; exclude them from lint+format. # Alembic migrations are autogenerated-style history; exclude them from lint+format.
extend-exclude = ["migrations"] extend-exclude = ["migrations"]

View file

@ -93,7 +93,7 @@ def safe_body(
if isinstance(body, Mapping | list): if isinstance(body, Mapping | list):
try: try:
text = json.dumps(redact_json(body), ensure_ascii=False, default=str) text = json.dumps(redact_json(body), ensure_ascii=False, default=str)
except (TypeError, ValueError): except TypeError, ValueError:
text = str(body) text = str(body)
return _truncate(text, max_chars) return _truncate(text, max_chars)
if isinstance(body, bytes): if isinstance(body, bytes):

View file

@ -48,7 +48,7 @@ def verify_password(plain: str, hashed: str) -> bool:
return _hasher.verify(hashed, plain) return _hasher.verify(hashed, plain)
except VerifyMismatchError: except VerifyMismatchError:
return False return False
except (VerificationError, InvalidHash): except VerificationError, InvalidHash:
return False return False
@ -56,7 +56,7 @@ def needs_rehash(hashed: str) -> bool:
"""True if the stored hash uses outdated params and should be re-hashed on next login.""" """True if the stored hash uses outdated params and should be re-hashed on next login."""
try: try:
return _hasher.check_needs_rehash(hashed) return _hasher.check_needs_rehash(hashed)
except (InvalidHash, TypeError): except InvalidHash, TypeError:
return False return False

View file

@ -28,15 +28,11 @@ from src.contract_check.worker_prescreen.extractor import (
from src.contract_check.worker_prescreen.extractor_heuristic import ( from src.contract_check.worker_prescreen.extractor_heuristic import (
EXTRACTOR_VERSION as HEURISTIC_VERSION, EXTRACTOR_VERSION as HEURISTIC_VERSION,
) )
from src.contract_check.worker_prescreen.extractor_heuristic import ( from src.contract_check.worker_prescreen.extractor_heuristic import HeuristicExtractor
HeuristicExtractor,
)
from src.contract_check.worker_prescreen.extractor_llm import ( from src.contract_check.worker_prescreen.extractor_llm import (
EXTRACTOR_VERSION as HYBRID_LLM_VERSION, EXTRACTOR_VERSION as HYBRID_LLM_VERSION,
) )
from src.contract_check.worker_prescreen.extractor_llm import ( from src.contract_check.worker_prescreen.extractor_llm import LLMPrescreenExtractor
LLMPrescreenExtractor,
)
log = get_logger(__name__) log = get_logger(__name__)

View file

@ -1,7 +1,7 @@
# syntax=docker/dockerfile:1 # syntax=docker/dockerfile:1
# ─── Stage 1: build deps + project into a venv via uv ───────────────────────── # ─── Stage 1: build deps + project into a venv via uv ─────────────────────────
FROM ghcr.io/astral-sh/uv:python3.13-bookworm-slim AS builder FROM ghcr.io/astral-sh/uv:python3.14-trixie-slim AS builder
ENV UV_COMPILE_BYTECODE=1 \ ENV UV_COMPILE_BYTECODE=1 \
UV_LINK_MODE=copy \ UV_LINK_MODE=copy \
@ -20,7 +20,7 @@ RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --frozen --no-default-groups --group api --no-install-project uv sync --frozen --no-default-groups --group api --no-install-project
# ─── Stage 2: lean runtime ───────────────────────────────────────────────── # ─── Stage 2: lean runtime ─────────────────────────────────────────────────
FROM python:3.13-slim FROM python:3.14-slim-trixie
ENV PYTHONDONTWRITEBYTECODE=1 \ ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \ PYTHONUNBUFFERED=1 \

View file

@ -1,7 +1,7 @@
# syntax=docker/dockerfile:1 # syntax=docker/dockerfile:1
# ─── Stage 1: build deps + project into a venv via uv ───────────────────────── # ─── Stage 1: build deps + project into a venv via uv ─────────────────────────
FROM ghcr.io/astral-sh/uv:python3.13-bookworm-slim AS builder FROM ghcr.io/astral-sh/uv:python3.14-trixie-slim AS builder
ENV UV_COMPILE_BYTECODE=1 \ ENV UV_COMPILE_BYTECODE=1 \
UV_LINK_MODE=copy \ UV_LINK_MODE=copy \
@ -19,7 +19,7 @@ RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --frozen --no-default-groups --group bot --no-install-project uv sync --frozen --no-default-groups --group bot --no-install-project
# ─── Stage 2: lean runtime ───────────────────────────────────────────────── # ─── Stage 2: lean runtime ─────────────────────────────────────────────────
FROM python:3.13-slim FROM python:3.14-slim-trixie
ENV PYTHONDONTWRITEBYTECODE=1 \ ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \ PYTHONUNBUFFERED=1 \

View file

@ -1,7 +1,7 @@
# syntax=docker/dockerfile:1 # syntax=docker/dockerfile:1
# ─── Stage 1: build deps + project into a venv via uv ───────────────────────── # ─── Stage 1: build deps + project into a venv via uv ─────────────────────────
FROM ghcr.io/astral-sh/uv:python3.13-bookworm-slim AS builder FROM ghcr.io/astral-sh/uv:python3.14-trixie-slim AS builder
ENV UV_COMPILE_BYTECODE=1 \ ENV UV_COMPILE_BYTECODE=1 \
UV_LINK_MODE=copy \ UV_LINK_MODE=copy \
@ -19,7 +19,7 @@ RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --frozen --no-default-groups --group analyze --no-install-project uv sync --frozen --no-default-groups --group analyze --no-install-project
# ─── Stage 2: lean runtime ───────────────────────────────────────────────── # ─── Stage 2: lean runtime ─────────────────────────────────────────────────
FROM python:3.13-slim FROM python:3.14-slim-trixie
ENV PYTHONDONTWRITEBYTECODE=1 \ ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \ PYTHONUNBUFFERED=1 \

View file

@ -1,7 +1,7 @@
# syntax=docker/dockerfile:1 # syntax=docker/dockerfile:1
# ─── Stage 1: build deps + project into a venv via uv ───────────────────────── # ─── Stage 1: build deps + project into a venv via uv ─────────────────────────
FROM ghcr.io/astral-sh/uv:python3.13-bookworm-slim AS builder FROM ghcr.io/astral-sh/uv:python3.14-trixie-slim AS builder
ENV UV_COMPILE_BYTECODE=1 \ ENV UV_COMPILE_BYTECODE=1 \
UV_LINK_MODE=copy \ UV_LINK_MODE=copy \
@ -17,7 +17,7 @@ RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --frozen --no-default-groups --group billing --no-install-project uv sync --frozen --no-default-groups --group billing --no-install-project
# ─── Stage 2: lean runtime ───────────────────────────────────────────────── # ─── Stage 2: lean runtime ─────────────────────────────────────────────────
FROM python:3.13-slim FROM python:3.14-slim-trixie
ENV PYTHONDONTWRITEBYTECODE=1 \ ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \ PYTHONUNBUFFERED=1 \

View file

@ -1,7 +1,7 @@
# syntax=docker/dockerfile:1 # syntax=docker/dockerfile:1
# ─── Stage 1: build deps + project into a venv via uv ───────────────────────── # ─── Stage 1: build deps + project into a venv via uv ─────────────────────────
FROM ghcr.io/astral-sh/uv:python3.13-bookworm-slim AS builder FROM ghcr.io/astral-sh/uv:python3.14-trixie-slim AS builder
ENV UV_COMPILE_BYTECODE=1 \ ENV UV_COMPILE_BYTECODE=1 \
UV_LINK_MODE=copy \ UV_LINK_MODE=copy \
@ -18,7 +18,7 @@ RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --frozen --no-default-groups --group extract --no-install-project uv sync --frozen --no-default-groups --group extract --no-install-project
# ─── Stage 2: runtime with tesseract-ocr + language packs ──────────────────── # ─── Stage 2: runtime with tesseract-ocr + language packs ────────────────────
FROM python:3.13-slim FROM python:3.14-slim-trixie
ENV PYTHONDONTWRITEBYTECODE=1 \ ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \ PYTHONUNBUFFERED=1 \
@ -33,7 +33,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
tesseract-ocr-rus \ tesseract-ocr-rus \
tesseract-ocr-eng \ tesseract-ocr-eng \
fonts-dejavu-core \ fonts-dejavu-core \
libmagic1 \ libmagic1t64 \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
COPY --from=builder /app/.venv /app/.venv COPY --from=builder /app/.venv /app/.venv

View file

@ -1,7 +1,7 @@
# syntax=docker/dockerfile:1 # syntax=docker/dockerfile:1
# ─── Stage 1: build deps + project into a venv via uv ───────────────────────── # ─── Stage 1: build deps + project into a venv via uv ─────────────────────────
FROM ghcr.io/astral-sh/uv:python3.13-bookworm-slim AS builder FROM ghcr.io/astral-sh/uv:python3.14-trixie-slim AS builder
ENV UV_COMPILE_BYTECODE=1 \ ENV UV_COMPILE_BYTECODE=1 \
UV_LINK_MODE=copy \ UV_LINK_MODE=copy \
@ -18,7 +18,7 @@ RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --frozen --no-default-groups --group notify --no-install-project uv sync --frozen --no-default-groups --group notify --no-install-project
# ─── Stage 2: lean runtime ───────────────────────────────────────────────── # ─── Stage 2: lean runtime ─────────────────────────────────────────────────
FROM python:3.13-slim FROM python:3.14-slim-trixie
ENV PYTHONDONTWRITEBYTECODE=1 \ ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \ PYTHONUNBUFFERED=1 \

View file

@ -1,7 +1,7 @@
# syntax=docker/dockerfile:1 # syntax=docker/dockerfile:1
# ─── Stage 1: build deps + project into a venv via uv ───────────────────────── # ─── Stage 1: build deps + project into a venv via uv ─────────────────────────
FROM ghcr.io/astral-sh/uv:python3.13-bookworm-slim AS builder FROM ghcr.io/astral-sh/uv:python3.14-trixie-slim AS builder
ENV UV_COMPILE_BYTECODE=1 \ ENV UV_COMPILE_BYTECODE=1 \
UV_LINK_MODE=copy \ UV_LINK_MODE=copy \
@ -18,7 +18,7 @@ RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --frozen --no-default-groups --group prescreen --no-install-project uv sync --frozen --no-default-groups --group prescreen --no-install-project
# ─── Stage 2: lean runtime ───────────────────────────────────────────────── # ─── Stage 2: lean runtime ─────────────────────────────────────────────────
FROM python:3.13-slim FROM python:3.14-slim-trixie
ENV PYTHONDONTWRITEBYTECODE=1 \ ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \ PYTHONUNBUFFERED=1 \

1181
uv.lock generated

File diff suppressed because it is too large Load diff