Skip to content

feat: add FastAPI anonymizer server#2039

Open
ynachiket wants to merge 5 commits into
microsoft:mainfrom
ynachiket:ynachiket/fastapi-anonymizer-slice-1769-20260525
Open

feat: add FastAPI anonymizer server#2039
ynachiket wants to merge 5 commits into
microsoft:mainfrom
ynachiket:ynachiket/fastapi-anonymizer-slice-1769-20260525

Conversation

@ynachiket
Copy link
Copy Markdown
Contributor

@ynachiket ynachiket commented May 25, 2026

Change

Adds an optional FastAPI server for presidio-anonymizer as a first narrow slice of #1769. The new fastapi_app.py mirrors the existing Flask anonymizer REST endpoints:

  • GET /health
  • POST /anonymize
  • POST /deanonymize
  • GET /anonymizers
  • GET /deanonymizers

The existing Flask app and Docker entrypoint are unchanged. FastAPI and Uvicorn are exposed through a new optional fastapi extra, and the README includes a minimal run command.

Validation

  • /tmp/presidio-fastapi-1769-py312/bin/python -m py_compile presidio-anonymizer/fastapi_app.py presidio-anonymizer/tests/test_fastapi_app.py
  • cd presidio-anonymizer && /tmp/presidio-fastapi-1769-py312/bin/python -m pytest tests/test_fastapi_app.py -q -> 4 passed
  • cd presidio-anonymizer && /tmp/presidio-fastapi-1769-py312/bin/ruff check fastapi_app.py tests/test_fastapi_app.py
  • git diff --check

Closes #1769? No, this is intentionally a first anonymizer-only slice toward the broader multi-service request.

@ynachiket ynachiket requested a review from a team as a code owner May 25, 2026 12:01
@omri374 omri374 requested a review from Copilot May 25, 2026 13:22
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an optional FastAPI-based REST server for presidio-anonymizer, intended to mirror the existing Flask anonymizer service endpoints as an initial slice toward #1769.

Changes:

  • Introduces fastapi_app.py implementing /health, /anonymize, /deanonymize, /anonymizers, and /deanonymizers in FastAPI.
  • Adds a new fastapi optional dependency extra (FastAPI + Uvicorn).
  • Adds a minimal README run snippet and a new test module for the FastAPI app.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 5 comments.

File Description
presidio-anonymizer/fastapi_app.py New FastAPI server implementation mirroring the existing Flask routes and error shapes.
presidio-anonymizer/pyproject.toml Adds fastapi optional-dependency extra for running the FastAPI server.
presidio-anonymizer/README.md Documents how to run the optional FastAPI server with Uvicorn.
presidio-anonymizer/tests/test_fastapi_app.py Adds initial tests for the FastAPI server endpoints and error response shape.

Comment on lines +52 to +56
@self.app.post("/anonymize")
def anonymize(content: dict[str, Any]) -> Response:
if not content:
raise HTTPException(status_code=400, detail="Invalid request json")

Comment on lines +6 to +10
import pytest

pytest.importorskip("httpx")
fastapi_testclient = pytest.importorskip("fastapi.testclient")
TestClient = fastapi_testclient.TestClient
spec = util.spec_from_file_location("presidio_anonymizer_fastapi_app", module_path)
module = util.module_from_spec(spec)
spec.loader.exec_module(module)
return module.create_app()
Comment thread presidio-anonymizer/README.md Outdated
Comment on lines +208 to +215
The anonymizer package also includes a FastAPI server with the same REST endpoints
as the default Flask server. Install the optional dependencies and run it with
Uvicorn:

```sh
pip install "presidio-anonymizer[fastapi]"
uvicorn fastapi_app:app --host 0.0.0.0 --port 3000
```
Comment on lines +31 to +74
def test_anonymize_endpoint_returns_engine_response():
"""Anonymize endpoint returns the anonymizer engine JSON response."""
client = TestClient(_load_fastapi_app())

response = client.post(
"/anonymize",
json={
"text": "My name is Jane",
"analyzer_results": [
{
"start": 11,
"end": 15,
"score": 0.8,
"entity_type": "PERSON",
}
],
"anonymizers": {
"DEFAULT": {"type": "replace", "new_value": "<ANONYMIZED>"}
},
},
)

assert response.status_code == 200
assert response.json()["text"] == "My name is <ANONYMIZED>"


def test_empty_json_body_returns_flask_compatible_error_shape():
"""Empty JSON requests keep the existing error response shape."""
client = TestClient(_load_fastapi_app())

response = client.post("/anonymize", json={})

assert response.status_code == 400
assert response.json() == {"error": "Invalid request json"}


def test_anonymizers_endpoint_returns_supported_operators():
"""Anonymizers endpoint exposes built-in anonymizer operators."""
client = TestClient(_load_fastapi_app())

response = client.get("/anonymizers")

assert response.status_code == 200
assert "replace" in response.json()
@ynachiket
Copy link
Copy Markdown
Contributor Author

Addressed the Copilot review feedback in 52e7f57:

  • Parse FastAPI request JSON manually so missing/invalid JSON returns the Flask-compatible {"error": "Invalid request json"} response shape.
  • Reuse the import-time module.app in the FastAPI tests to avoid duplicate app factory initialization.
  • Added coverage for missing/invalid JSON, /deanonymize, and /deanonymizers.
  • Added fastapi and httpx to the anonymizer dev dependencies so the new FastAPI tests run in CI instead of being skipped.
  • Clarified that uvicorn fastapi_app:app is a source-checkout command from presidio-anonymizer/.

Validation:

  • /tmp/presidio-pdlc045-py312/bin/python -m py_compile presidio-anonymizer/fastapi_app.py presidio-anonymizer/tests/test_fastapi_app.py
  • PYTHONPATH=presidio-anonymizer /tmp/presidio-pdlc045-py312/bin/python -m pytest presidio-anonymizer/tests/test_fastapi_app.py -q -> 8 passed
  • git diff --check

GitHub checks had not been reported yet immediately after push.

Copilot AI review requested due to automatic review settings June 7, 2026 06:49
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

Comment on lines +53 to +76
@self.app.post("/anonymize")
async def anonymize(request: Request) -> Response:
content = await self._json_request_body(request)

anonymizers_config = AppEntitiesConvertor.operators_config_from_json(
content.get("anonymizers")
)
if AppEntitiesConvertor.check_custom_operator(anonymizers_config):
raise HTTPException(
status_code=400, detail="Custom type anonymizer is not supported"
)

analyzer_results = AppEntitiesConvertor.analyzer_results_from_json(
content.get("analyzer_results")
)
anonymizer_result = self.anonymizer.anonymize(
text=content.get("text", ""),
analyzer_results=analyzer_results,
operators=anonymizers_config,
)
return Response(
content=anonymizer_result.to_json(), media_type="application/json"
)

Comment on lines +77 to +96
@self.app.post("/deanonymize")
async def deanonymize(request: Request) -> Response:
content = await self._json_request_body(request)

deanonymize_entities = AppEntitiesConvertor.deanonymize_entities_from_json(
content
)
deanonymize_config = AppEntitiesConvertor.operators_config_from_json(
content.get("deanonymizers")
)
deanonymized_response = self.deanonymize.deanonymize(
text=content.get("text", ""),
entities=deanonymize_entities,
operators=deanonymize_config,
)
return Response(
content=deanonymized_response.to_json(),
media_type="application/json",
)

Comment on lines +129 to +132
@self.app.exception_handler(Exception)
def server_error(_: Request, err: Exception) -> JSONResponse:
self.logger.error("A fatal error occurred during execution: %s", err)
return JSONResponse({"error": "Internal server error"}, status_code=500)
Copilot AI review requested due to automatic review settings June 7, 2026 12:09
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.

Comment on lines +53 to +75
@self.app.post("/anonymize")
async def anonymize(request: Request) -> Response:
content = await self._json_request_body(request)

anonymizers_config = AppEntitiesConvertor.operators_config_from_json(
content.get("anonymizers")
)
if AppEntitiesConvertor.check_custom_operator(anonymizers_config):
raise HTTPException(
status_code=400, detail="Custom type anonymizer is not supported"
)

analyzer_results = AppEntitiesConvertor.analyzer_results_from_json(
content.get("analyzer_results")
)
anonymizer_result = self.anonymizer.anonymize(
text=content.get("text", ""),
analyzer_results=analyzer_results,
operators=anonymizers_config,
)
return Response(
content=anonymizer_result.to_json(), media_type="application/json"
)
Comment on lines +77 to +96
@self.app.post("/deanonymize")
async def deanonymize(request: Request) -> Response:
content = await self._json_request_body(request)

deanonymize_entities = AppEntitiesConvertor.deanonymize_entities_from_json(
content
)
deanonymize_config = AppEntitiesConvertor.operators_config_from_json(
content.get("deanonymizers")
)
deanonymized_response = self.deanonymize.deanonymize(
text=content.get("text", ""),
entities=deanonymize_entities,
operators=deanonymize_config,
)
return Response(
content=deanonymized_response.to_json(),
media_type="application/json",
)

Comment on lines +52 to +53
fastapi = "*"
httpx = "*"
directory, install the optional dependencies and run it with Uvicorn:

```sh
pip install "presidio-anonymizer[fastapi]"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FR] FastAPI Implementation for all services

3 participants