Spaces:
Build error
Build error
Claude commited on
fix(ingest): add User-Agent and headers to httpx requests to fix Gallica 403
Browse filesGallica (BnF) rejects automated requests without a valid User-Agent.
Added _HEADERS to iiif_fetcher.py (image download) and _MANIFEST_HEADERS to
ingest.py (_fetch_json_manifest) with:
- User-Agent: Mozilla/5.0 (compatible; ScriptoriumAI/1.0; ...)
- Accept: appropriate MIME types for each request type
- Referer: https://gallica.bnf.fr/
Updated test_image_pipeline.py to assert the headers are passed in the mock call.
https://claude.ai/code/session_018woyEHc8HG2th7V4ewJ4Kg
backend/app/api/v1/ingest.py
CHANGED
|
@@ -112,10 +112,20 @@ async def _create_page(
|
|
| 112 |
return page
|
| 113 |
|
| 114 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
async def _fetch_json_manifest(url: str) -> dict:
|
| 116 |
"""Télécharge un manifest IIIF. Fonction isolée pour faciliter les tests."""
|
| 117 |
async with httpx.AsyncClient() as client:
|
| 118 |
-
resp = await client.get(url, follow_redirects=True, timeout=30.0)
|
| 119 |
resp.raise_for_status()
|
| 120 |
return resp.json()
|
| 121 |
|
|
|
|
| 112 |
return page
|
| 113 |
|
| 114 |
|
| 115 |
+
_MANIFEST_HEADERS = {
|
| 116 |
+
"User-Agent": (
|
| 117 |
+
"Mozilla/5.0 (compatible; ScriptoriumAI/1.0; "
|
| 118 |
+
"+https://huggingface.co/spaces/Ma-Ri-Ba-Ku/scriptorium-ai)"
|
| 119 |
+
),
|
| 120 |
+
"Accept": "application/ld+json,application/json,*/*",
|
| 121 |
+
"Referer": "https://gallica.bnf.fr/",
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
|
| 125 |
async def _fetch_json_manifest(url: str) -> dict:
|
| 126 |
"""Télécharge un manifest IIIF. Fonction isolée pour faciliter les tests."""
|
| 127 |
async with httpx.AsyncClient() as client:
|
| 128 |
+
resp = await client.get(url, headers=_MANIFEST_HEADERS, follow_redirects=True, timeout=30.0)
|
| 129 |
resp.raise_for_status()
|
| 130 |
return resp.json()
|
| 131 |
|
backend/app/services/ingest/iiif_fetcher.py
CHANGED
|
@@ -11,6 +11,15 @@ logger = logging.getLogger(__name__)
|
|
| 11 |
|
| 12 |
_DEFAULT_TIMEOUT = 60.0 # secondes — les images IIIF haute résolution peuvent être lourdes
|
| 13 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
|
| 15 |
def fetch_iiif_image(url: str, timeout: float = _DEFAULT_TIMEOUT) -> bytes:
|
| 16 |
"""Télécharge une image depuis une URL IIIF complète.
|
|
@@ -28,7 +37,7 @@ def fetch_iiif_image(url: str, timeout: float = _DEFAULT_TIMEOUT) -> bytes:
|
|
| 28 |
httpx.RequestError: pour toute autre erreur réseau.
|
| 29 |
"""
|
| 30 |
logger.info("Fetching IIIF image", extra={"url": url})
|
| 31 |
-
response = httpx.get(url, follow_redirects=True, timeout=timeout)
|
| 32 |
response.raise_for_status()
|
| 33 |
logger.info(
|
| 34 |
"IIIF image fetched",
|
|
|
|
| 11 |
|
| 12 |
_DEFAULT_TIMEOUT = 60.0 # secondes — les images IIIF haute résolution peuvent être lourdes
|
| 13 |
|
| 14 |
+
_HEADERS = {
|
| 15 |
+
"User-Agent": (
|
| 16 |
+
"Mozilla/5.0 (compatible; ScriptoriumAI/1.0; "
|
| 17 |
+
"+https://huggingface.co/spaces/Ma-Ri-Ba-Ku/scriptorium-ai)"
|
| 18 |
+
),
|
| 19 |
+
"Accept": "image/jpeg,image/png,image/*,*/*",
|
| 20 |
+
"Referer": "https://gallica.bnf.fr/",
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
|
| 24 |
def fetch_iiif_image(url: str, timeout: float = _DEFAULT_TIMEOUT) -> bytes:
|
| 25 |
"""Télécharge une image depuis une URL IIIF complète.
|
|
|
|
| 37 |
httpx.RequestError: pour toute autre erreur réseau.
|
| 38 |
"""
|
| 39 |
logger.info("Fetching IIIF image", extra={"url": url})
|
| 40 |
+
response = httpx.get(url, headers=_HEADERS, follow_redirects=True, timeout=timeout)
|
| 41 |
response.raise_for_status()
|
| 42 |
logger.info(
|
| 43 |
"IIIF image fetched",
|
backend/tests/test_image_pipeline.py
CHANGED
|
@@ -272,6 +272,14 @@ def test_fetch_iiif_image_success():
|
|
| 272 |
assert result == fake_bytes
|
| 273 |
mock_get.assert_called_once_with(
|
| 274 |
"https://example.com/image.jpg",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 275 |
follow_redirects=True,
|
| 276 |
timeout=60.0,
|
| 277 |
)
|
|
|
|
| 272 |
assert result == fake_bytes
|
| 273 |
mock_get.assert_called_once_with(
|
| 274 |
"https://example.com/image.jpg",
|
| 275 |
+
headers={
|
| 276 |
+
"User-Agent": (
|
| 277 |
+
"Mozilla/5.0 (compatible; ScriptoriumAI/1.0; "
|
| 278 |
+
"+https://huggingface.co/spaces/Ma-Ri-Ba-Ku/scriptorium-ai)"
|
| 279 |
+
),
|
| 280 |
+
"Accept": "image/jpeg,image/png,image/*,*/*",
|
| 281 |
+
"Referer": "https://gallica.bnf.fr/",
|
| 282 |
+
},
|
| 283 |
follow_redirects=True,
|
| 284 |
timeout=60.0,
|
| 285 |
)
|