Skip to content

Commit 77b8e57

Browse files
improvement(presidio): address review feedback
- Register VIN under all served languages, not just en (Bugbot: VIN missed for non-English language routing). - Bump HEALTHCHECK start-period to 180s — five lg models load at import (Bugbot). - Drop --no-cache-dir so the pip cache mount actually works (Greptile). - Pydantic request models for /analyze + /anonymize so missing 'text' returns 422 not 500; default operator 'type' to 'replace' instead of KeyError->500 (Greptile).
1 parent 815d875 commit 77b8e57

2 files changed

Lines changed: 46 additions & 24 deletions

File tree

docker/presidio.Dockerfile

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
1616
# don't reinstall the heavy model.
1717
COPY docker/presidio/requirements.txt ./requirements.txt
1818
RUN --mount=type=cache,target=/root/.cache/pip \
19-
pip install --no-cache-dir -r requirements.txt
19+
pip install -r requirements.txt
2020

2121
# Pinned spaCy models (en + es/it/pl/fi, ~2.2GB total). Downloaded with
2222
# retries/resume — the large wheels truncate on flaky networks if pip fetches
@@ -29,7 +29,7 @@ RUN --mount=type=cache,target=/root/.cache/pip \
2929
-o "/tmp/${whl}" \
3030
"https://github.com/explosion/spacy-models/releases/download/${model}/${whl}" || exit 1; \
3131
done && \
32-
pip install --no-cache-dir /tmp/*.whl && \
32+
pip install /tmp/*.whl && \
3333
rm /tmp/*.whl
3434

3535
COPY docker/presidio/server.py ./server.py
@@ -41,7 +41,9 @@ USER presidio
4141

4242
EXPOSE 3000
4343

44-
HEALTHCHECK --interval=30s --timeout=5s --start-period=40s --retries=3 \
44+
# start-period is generous: five large spaCy models load at import before
45+
# /health responds. Tune against measured cold-start once built.
46+
HEALTHCHECK --interval=30s --timeout=5s --start-period=180s --retries=3 \
4547
CMD curl -fsS http://localhost:3000/health || exit 1
4648

4749
CMD ["uvicorn", "server:app", "--host", "0.0.0.0", "--port", "3000"]

docker/presidio/server.py

Lines changed: 41 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
from typing import Any
99

10-
from fastapi import Body, FastAPI
10+
from fastapi import FastAPI
1111
from presidio_analyzer import AnalyzerEngine, Pattern, PatternRecognizer, RecognizerResult
1212
from presidio_analyzer.nlp_engine import NlpEngineProvider
1313
from presidio_analyzer.predefined_recognizers import (
@@ -35,6 +35,7 @@
3535
)
3636
from presidio_anonymizer import AnonymizerEngine
3737
from presidio_anonymizer.entities import OperatorConfig
38+
from pydantic import BaseModel
3839

3940
# Languages served. Each needs its spaCy model installed in the image; the
4041
# es/it/pl/fi predefined recognizers (ES_NIF, IT_FISCAL_CODE, PL_PESEL, ...)
@@ -112,14 +113,18 @@ def validate_result(self, pattern_text: str):
112113
def build_analyzer() -> AnalyzerEngine:
113114
nlp_engine = NlpEngineProvider(nlp_configuration=NLP_CONFIGURATION).create_engine()
114115
analyzer = AnalyzerEngine(nlp_engine=nlp_engine, supported_languages=SUPPORTED_LANGUAGES)
116+
# VIN is language-agnostic, so register it under every served language —
117+
# a recognizer only fires for the language the caller routes to.
115118
vin_pattern = Pattern(name="vin", regex=r"\b[A-HJ-NPR-Z0-9]{17}\b", score=0.7)
116-
analyzer.registry.add_recognizer(
117-
VinRecognizer(
118-
supported_entity="VIN",
119-
patterns=[vin_pattern],
120-
context=["vin", "vehicle", "chassis"],
119+
for language in SUPPORTED_LANGUAGES:
120+
analyzer.registry.add_recognizer(
121+
VinRecognizer(
122+
supported_entity="VIN",
123+
patterns=[vin_pattern],
124+
context=["vin", "vehicle", "chassis"],
125+
supported_language=language,
126+
)
121127
)
122-
)
123128
for recognizer_cls in EXTRA_RECOGNIZERS:
124129
analyzer.registry.add_recognizer(recognizer_cls())
125130
return analyzer
@@ -131,6 +136,21 @@ def build_analyzer() -> AnalyzerEngine:
131136
app = FastAPI(title="Sim Presidio", docs_url=None, redoc_url=None)
132137

133138

139+
class AnalyzeRequest(BaseModel):
140+
text: str
141+
language: str = "en"
142+
entities: list[str] | None = None
143+
score_threshold: float | None = None
144+
return_decision_process: bool = False
145+
146+
147+
class AnonymizeRequest(BaseModel):
148+
text: str
149+
analyzer_results: list[dict[str, Any]] = []
150+
anonymizers: dict[str, dict[str, Any]] | None = None
151+
operators: dict[str, dict[str, Any]] | None = None
152+
153+
134154
@app.get("/health")
135155
def health() -> dict[str, str]:
136156
return {"status": "ok"}
@@ -142,38 +162,38 @@ def supported_entities(language: str = "en") -> list[str]:
142162

143163

144164
@app.post("/analyze")
145-
def analyze(payload: dict[str, Any] = Body(...)) -> list[dict[str, Any]]:
146-
entities = payload.get("entities") or None
165+
def analyze(req: AnalyzeRequest) -> list[dict[str, Any]]:
147166
results = analyzer.analyze(
148-
text=payload["text"],
149-
language=payload.get("language", "en"),
150-
entities=entities,
151-
score_threshold=payload.get("score_threshold"),
152-
return_decision_process=payload.get("return_decision_process", False),
167+
text=req.text,
168+
language=req.language,
169+
entities=req.entities or None,
170+
score_threshold=req.score_threshold,
171+
return_decision_process=req.return_decision_process,
153172
)
154173
return [r.to_dict() for r in results]
155174

156175

157176
@app.post("/anonymize")
158-
def anonymize(payload: dict[str, Any] = Body(...)) -> dict[str, Any]:
177+
def anonymize(req: AnonymizeRequest) -> dict[str, Any]:
159178
analyzer_results = [
160179
RecognizerResult(
161180
entity_type=r["entity_type"],
162181
start=r["start"],
163182
end=r["end"],
164183
score=r.get("score", 1.0),
165184
)
166-
for r in payload.get("analyzer_results", [])
185+
for r in req.analyzer_results
167186
]
168-
raw_operators = payload.get("anonymizers") or payload.get("operators")
187+
raw_operators = req.anonymizers or req.operators
169188
operators = None
170189
if raw_operators:
171190
operators = {}
172-
for entity, cfg in raw_operators.items():
173-
cfg = dict(cfg)
174-
operators[entity] = OperatorConfig(cfg.pop("type"), cfg)
191+
for entity, raw_cfg in raw_operators.items():
192+
op_cfg = dict(raw_cfg)
193+
op_type = op_cfg.pop("type", "replace")
194+
operators[entity] = OperatorConfig(op_type, op_cfg)
175195
result = anonymizer.anonymize(
176-
text=payload["text"],
196+
text=req.text,
177197
analyzer_results=analyzer_results,
178198
operators=operators,
179199
)

0 commit comments

Comments
 (0)