Correction suite
CI / Interface — types et compilation (push) Successful in 19s
CI / Backend — lint, types, tests (push) Successful in 36s
CI / Construction des images (sans publication) (push) Successful in 1m39s
Publication / Images vers Harbor (push) Successful in 2m58s

This commit is contained in:
hcornet committed 2026-09-12 13:52:59 +02:00
1 parent cd23176c9b
commit 6bb274b84b
9 files changed
+61 -38

No files matched your search

+3 -3
View File
@@ -45,9 +45,9 @@ jobs:
- name: Types (mypy)
working-directory: backend
# Non bloquant : le typage strict de SQLAlchemy et de Maigret produit
# du bruit qu'on ne veut pas traiter comme une régression.
continue-on-error: true
# Bloquant : le code passe mypy sans aucun signalement. Laisser l'étape
# en continue-on-error laisserait le typage se dégrader sans que
# personne ne le voie — une étape qui ne peut pas échouer ne sert à rien.
run: mypy src/limier
- name: Tests
+26 -21
View File
@@ -1,9 +1,10 @@
# Image unique pour l'API et le worker : même code, même base de sites, deux
# commandes. Deux images divergeraient à la première mise à jour oubliée.
#
# Construction en deux étapes : la couche de dépendances ne se reconstruit que
# lorsque pyproject.toml change, ce qui évite de retélécharger Maigret et ses
# 3 300 sites à chaque modification de code.
# Construction en deux étapes. L'étape « build » installe d'abord les seules
# dépendances, à partir de pyproject.toml : cette couche n'est refaite que si
# les dépendances changent, pas à chaque modification de code. Elle tire Maigret
# et sa base de 3 300 sites, c'est la plus coûteuse du projet.
FROM python:3.12-slim-bookworm AS base
@@ -20,7 +21,7 @@ RUN apt-get update \
&& ln -snf /usr/share/zoneinfo/$TZ /etc/localtime \
&& rm -rf /var/lib/apt/lists/*
# ------------------------------------------------------------------ build
# ------------------------------------------------------------------- build
FROM base AS build
RUN apt-get update \
@@ -28,30 +29,35 @@ RUN apt-get update \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /build
# 1. Dépendances seules. La liste est extraite de pyproject.toml pour rester la
# source unique de vérité — pas de requirements.txt parallèle qui dériverait.
# Commande sur une seule ligne : un heredoc imbriqué ici serait interprété
# par l'analyseur du Dockerfile, pas par le shell.
COPY pyproject.toml ./
COPY src/limier/__init__.py src/limier/__init__.py
RUN python -c "import tomllib,pathlib;print(chr(10).join(tomllib.loads(pathlib.Path('pyproject.toml').read_text())['project']['dependencies']))" > /tmp/requirements.txt \
&& cat /tmp/requirements.txt \
&& pip install --prefix=/opt/limier --no-warn-script-location -r /tmp/requirements.txt
# Installation des dépendances seules, dans un préfixe isolé et copiable.
RUN pip install --prefix=/opt/limier --no-warn-script-location \
"$(python - <<'PY'
import tomllib, pathlib
d = tomllib.loads(pathlib.Path("pyproject.toml").read_text())
print(" ".join(d["project"]["dependencies"]))
PY
)"
# 2. Le paquet lui-même, sans ses dépendances déjà présentes. Installe aussi les
# commandes limier-api et limier-worker.
COPY src ./src
RUN pip install --prefix=/opt/limier --no-warn-script-location --no-deps .
# ---------------------------------------------------------------- runtime
# ----------------------------------------------------------------- runtime
FROM base AS runtime
# /opt/limier/lib/python3.12/site-packages devient
# /usr/local/lib/python3.12/site-packages, l'emplacement standard de l'image.
COPY --from=build /opt/limier /usr/local
WORKDIR /app
COPY pyproject.toml alembic.ini ./
# alembic.ini et migrations/ ne sont pas dans le paquet : ils sont nécessaires
# au runtime pour « alembic upgrade head ».
COPY alembic.ini ./
COPY migrations ./migrations
COPY src ./src
RUN pip install --no-deps -e . \
&& groupadd -r limier -g 1000 \
RUN groupadd -r limier -g 1000 \
&& useradd -r -u 1000 -g limier -d /app -s /sbin/nologin limier \
&& mkdir -p /data/sites \
&& chown -R limier:limier /app /data
@@ -59,9 +65,8 @@ RUN pip install --no-deps -e . \
USER limier
# Base de sites inscriptible : l'auto-contrôle nocturne y écrit la version
# corrigée. Le paquet Maigret, lui, est en lecture seule.
ENV LIMIER_SITES_DB_PATH=/data/sites/data.json \
PYTHONPATH=/app/src
# corrigée. Le paquet Maigret, lui, reste en lecture seule.
ENV LIMIER_SITES_DB_PATH=/data/sites/data.json
EXPOSE 8000
+3 -1
View File
@@ -7,6 +7,8 @@ réellement actifs. Une instance sans clé HIBP n'affiche pas la case « fuites
from __future__ import annotations
from typing import Any
from fastapi import APIRouter
from limier.api.schemas import InfoCatalogue
@@ -22,7 +24,7 @@ async def catalogue() -> InfoCatalogue:
settings = get_settings()
stats = {"total": 0, "actifs": 0, "tags": {}}
stats: dict[str, Any] = {"total": 0, "actifs": 0, "tags": {}}
if settings.engine_username_enabled:
from limier.engines.username import database
+5 -1
View File
@@ -141,8 +141,12 @@ async def valider_jeton_identite(jeton: str) -> dict[str, Any]:
except Exception as exc:
raise ErreurAuthentification(f"Jeton d'identité invalide : {exc}") from exc
emetteur = config.get("issuer")
if not emetteur:
raise ErreurAuthentification("Le document de découverte OIDC ne déclare pas d'émetteur.")
registre = JWTClaimsRegistry(
iss={"essential": True, "value": config.get("issuer")},
iss={"essential": True, "value": emetteur},
aud={"essential": True, "values": [settings.oidc_client_id]},
exp={"essential": True},
sub={"essential": True},
+5 -2
View File
@@ -34,10 +34,13 @@ class Settings(BaseSettings):
api_prefix: str = "/api"
# --------------------------------------------------------------- stockage
# Les valeurs par défaut sont des chaînes : Pydantic les valide et les
# convertit à l'instanciation. L'annotation reste le type DSN pour que la
# validation s'applique aussi aux valeurs venant de l'environnement.
database_url: PostgresDsn = Field(
default="postgresql+asyncpg://limier:limier@limier-postgres:5432/limier"
default="postgresql+asyncpg://limier:limier@limier-postgres:5432/limier" # type: ignore[assignment]
)
redis_url: RedisDsn = Field(default="redis://limier-redis:6379/0")
redis_url: RedisDsn = Field(default="redis://limier-redis:6379/0") # type: ignore[assignment]
# ------------------------------------------------------------------- auth
oidc_issuer: str = "https://authentik.tips-of-mine.com/application/o/limier/"
+10 -7
View File
@@ -65,19 +65,22 @@ class WorkerSettings:
max_jobs = get_settings().worker_max_jobs
job_timeout = get_settings().job_timeout_seconds
keep_result = get_settings().job_result_ttl_seconds
max_tries = 1 # une recherche échouée n'est pas rejouée : elle consommerait
# un second crédit et frapperait les sites une fois de plus
# Une recherche échouée n'est pas rejouée : elle consommerait un second
# crédit et frapperait les sites une fois de plus.
max_tries = 1
@staticmethod
def redis_settings() -> RedisSettings:
return parametres_redis()
# Attribut simple, et surtout PAS une staticmethod : arq construit le worker
# à partir de ``settings_cls.__dict__``, où une staticmethod est stockée
# telle quelle. Worker recevrait alors l'objet méthode au lieu des
# paramètres de connexion. Sous cette forme, le lancement par la commande
# ``arq limier.entrypoints.worker.WorkerSettings`` fonctionne aussi.
redis_settings: ClassVar[RedisSettings] = parametres_redis()
def main() -> None:
from arq.worker import run_worker
WorkerSettings.redis_settings = parametres_redis()
run_worker(WorkerSettings)
run_worker(WorkerSettings) # type: ignore[arg-type]
if __name__ == "__main__":
+2 -1
View File
@@ -72,7 +72,8 @@ async def historique(recherche_id: UUID | str, depuis: int = 0) -> list[str]:
"""Évènements déjà émis, à partir de l'index donné."""
cle_liste, _ = _cles(recherche_id)
redis = await get_redis()
return await redis.lrange(cle_liste, depuis, -1)
# redis-py type lrange en union sync/async ; le client est asynchrone ici.
return await redis.lrange(cle_liste, depuis, -1) # type: ignore[misc]
async def ecouter(recherche_id: UUID | str):
+1 -1
View File
@@ -60,7 +60,7 @@ def configurer() -> None:
for nom in ("httpx", "httpcore", "aiohttp", "asyncio", "arq"):
logging.getLogger(nom).setLevel(logging.WARNING)
processeurs = [
processeurs: list[Any] = [
structlog.contextvars.merge_contextvars,
structlog.stdlib.add_log_level,
structlog.stdlib.add_logger_name,
+6 -1
View File
@@ -38,7 +38,6 @@ import re
from typing import Any
import httpx
import trio_asyncio # noqa: F401 (holehe s'appuie sur trio ; voir plus bas)
from fastapi import FastAPI
from pydantic import BaseModel, Field
@@ -57,6 +56,12 @@ def charger_modules() -> list[Any]:
``async def <nom>(email, client, out)`` réparties dans
``holehe.modules.*``. On les découvre par introspection plutôt que de
maintenir une liste en dur qui divergerait à chaque mise à jour.
Vérifié sur holehe 1.61 : 121 modules découverts, aucun import en échec.
Le CLI de holehe s'exécute sous trio, mais les fonctions de module sont de
simples coroutines qui reçoivent un ``httpx.AsyncClient`` : elles tournent
donc sous asyncio sans passerelle, et ``trio_asyncio`` n'est pas nécessaire.
"""
global _modules
if _modules is not None: