Correction suite
This commit is contained in:
1 parent
cd23176c9b
commit
6bb274b84b
9 files changed
+61
-38
No files matched your search
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -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/"
|
||||
|
||||
@@ -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__":
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in new issue
Block a user