Téléchargement IGN atomique (.part + rename) et pagination du catalogue STAC

This commit is contained in:
Antoine Jacquin
2026-09-18 21:18:06 +02:00
parent 0dc485d0bf
commit 777b0c3f98

View File

@ -90,28 +90,47 @@ def match_feature(features, col, row):
return None return None
def find_tile_url(col, row, timeout=20): def find_tile_url(col, row, timeout=20, max_pages=5):
"""Cherche l'URL de téléchargement de la dalle (col,row) dans le catalogue STAC. """Cherche l'URL de téléchargement de la dalle (col,row) dans le catalogue STAC.
Suit la pagination du catalogue (lien rel=next) : sans elle, une dalle
au-delà de la première page de résultats semblait « introuvable ».
Returns: Returns:
URL (str) ou None si la dalle n'est pas (encore) publiée par l'IGN. URL (str) ou None si la dalle n'est pas (encore) publiée par l'IGN.
""" """
w, s, e, n = _bbox_wgs84(col, row) w, s, e, n = _bbox_wgs84(col, row)
query = urllib.parse.urlencode({"bbox": f"{w:.6f},{s:.6f},{e:.6f},{n:.6f}", "limit": 50}) query = urllib.parse.urlencode({"bbox": f"{w:.6f},{s:.6f},{e:.6f},{n:.6f}", "limit": 50})
req = urllib.request.Request(f"{_STAC_ITEMS_URL}?{query}", headers=_HEADERS) url = f"{_STAC_ITEMS_URL}?{query}"
for _ in range(max_pages):
req = urllib.request.Request(url, headers=_HEADERS)
with urllib.request.urlopen(req, timeout=timeout) as response: with urllib.request.urlopen(req, timeout=timeout) as response:
data = json.loads(response.read().decode("utf-8")) data = json.loads(response.read().decode("utf-8"))
feature = match_feature(data.get("features", []), col, row) feature = match_feature(data.get("features", []), col, row)
if not feature: if feature:
return None
return feature.get("assets", {}).get("data", {}).get("href") return feature.get("assets", {}).get("data", {}).get("href")
nxt = next((link.get("href") for link in data.get("links", [])
if link.get("rel") == "next" and link.get("href")), None)
if not nxt:
return None
url = nxt
return None
def download_file(url, dest_path, timeout=120, chunk=1024 * 1024): def download_file(url, dest_path, timeout=120, chunk=1024 * 1024):
"""Télécharge url vers dest_path en streaming. Retourne la taille en octets.""" """Télécharge url vers dest_path en streaming (écriture atomique).
Écrit dans un .part puis renomme : un run interrompu (SIGKILL, panne)
ne laisse jamais un LAZ partiel qu'un run suivant considérerait comme
déjà téléchargé. Retourne la taille en octets.
"""
import os
dest_path = Path(dest_path)
tmp = dest_path.with_name(dest_path.name + ".part")
req = urllib.request.Request(url, headers=_HEADERS) req = urllib.request.Request(url, headers=_HEADERS)
t0 = time.time() t0 = time.time()
with urllib.request.urlopen(req, timeout=timeout) as response, open(dest_path, "wb") as out: try:
with urllib.request.urlopen(req, timeout=timeout) as response, open(tmp, "wb") as out:
done = 0 done = 0
while True: while True:
block = response.read(chunk) block = response.read(chunk)
@ -122,6 +141,10 @@ def download_file(url, dest_path, timeout=120, chunk=1024 * 1024):
elapsed = time.time() - t0 elapsed = time.time() - t0
logger.info(f" {done / 1e6:.0f} Mo en {elapsed:.0f}s" logger.info(f" {done / 1e6:.0f} Mo en {elapsed:.0f}s"
f" ({done / 1e6 / max(elapsed, 0.1):.1f} Mo/s)") f" ({done / 1e6 / max(elapsed, 0.1):.1f} Mo/s)")
os.replace(tmp, dest_path)
except BaseException:
tmp.unlink(missing_ok=True)
raise
return done return done