Skip to content

Archive (pure functions)

egms_client.archive

Searching and downloading from the EGMS archive: Ortho L3 tiles and L2 bursts.

The Ortho L3 product is the ground velocity resampled on a 100 m grid in EPSG:3035 (LAEA Europe), in 100 km tiles, one per component: U (vertical) and E (east-west). A tile is named by its lower-left corner in hundreds of kilometres: E47N19 covers 4,700,000-4,800,000 m east and 1,900,000-2,000,000 m north. Each ZIP (~60 MB) holds a CSV of time series (~245 MB once unpacked) and a GeoTIFF of the mean velocity (~4 MB).

Downloading needs the id of a search, not the bearer token: the download URL carries ?id=<search id>, and that id expires. An expired or unknown id is answered with HTTP 401 and a plain-text message.

Verified against the live archive on 2026-11-03:

  • releases are named 2020-2025 in the API and 2020_2025 in file names; a release is a window of years, and a new one may extend the window rather than slide it (2020-2024 was followed by 2020-2025, not 2021-2025);
  • a tile at sea or outside coverage is answered with status: false and Invalid tile ID: 'E40N15', not with an empty search;
  • each hit carries its own filename and version (2020_2025_1): the trailing number is a sub-release, so read the name from the hit instead of building it.

Beyond Ortho L3 the archive serves two levels of measurement points, one ZIP per Sentinel-1 burst (a CSV of points with their time series and an XML of metadata): L2A (BASIC) and L2B (CALIBRATED, tied to GNSS). Verified on 2026-11-03:

  • GET /levels names them L2A, L2B, L3; the lower-case L2a of the file names is refused as Invalid level;
  • a search needs a place: bbox, tileId, swath, burstCycle or relativeOrbit, else You must provide either bbox, ...;
  • bbox is [[west, south], [east, north]] in degrees, for every level; any other shape is answered An error occurred processing the search, and an area of 7° x 7° is Requested area is too large (5° x 5° passes);
  • relativeOrbit is a string of three digits: "022" works, 22 and "22" are Invalid relative orbit;
  • one relative orbit is ~400 bursts and ~90 GB per level.

Query dataclass

What to look for: levels, a release and at least one place.

bbox works for every level; tile is for L3; relative_orbit, burst_cycle and swath are for L2A and L2B. product_type narrows L3 to ORTHO-UP or ORTHO-EAST.

Hit dataclass

One downloadable file of a search. The L2 fields are empty for L3.

stem property

stem: str

The file name without .zip: also the name of every file inside.

family property

family: str

The name up to the sub-release: hits of one family are versions of one file.

covers

covers(lon: float, lat: float) -> bool

True if the point is inside the footprint (inside the bbox if there is none).

Search dataclass

A search id and its hits. The id is what the download URL needs.

tile_id

tile_id(tile: Tile) -> str

(47, 19) → "E47N19".

parse_tile_id

parse_tile_id(text: str) -> Tile

"E47N19" → (47, 19); ValueError for anything else.

tile_for_point

tile_for_point(x: float, y: float) -> Tile

The tile containing a point in EPSG:3035 metres.

tiles_for_bounds

tiles_for_bounds(minx: float, miny: float, maxx: float, maxy: float) -> list[Tile]

The tiles covering a rectangle in EPSG:3035 metres.

api_release

api_release(release: str) -> str

2020_2025 (file names) or 2020-2025 → 2020-2025 (the API's form).

file_release

file_release(release: str) -> str

2020-2025 or 2020_2025 → 2020_2025 (the file names' form).

tile_prefix

tile_prefix(tile: Tile, component: Component, release: str) -> str

The file name up to the sub-release: EGMS_L3_E47N19_100km_U_2020_2025_.

search_body

search_body(tile: Tile, component: Component, release: str) -> dict[str, Any]

The JSON body of POST {archive}/search for one Ortho L3 tile.

query_body

query_body(query: Query) -> dict[str, Any]

The JSON body of POST {archive}/search for a :class:Query.

ValueError when the query has no place, which the archive would refuse.

split_bbox

split_bbox(bbox: LonLatBounds, step: float = MAX_BBOX_DEG) -> list[LonLatBounds]

A box cut into pieces no larger than step degrees a side, west to east.

parse_releases

parse_releases(body: bytes | str) -> list[str]

The release names, oldest first. :class:EgmsError when not logged in.

parse_names

parse_names(body: bytes | str, what: str) -> list[str]

A JSON list of names (/levels, /bursts), in the archive's order.

parse_search(body: bytes | str) -> Search | None

The search, or None when the tile does not exist (sea, no coverage).

Raises :class:EgmsError for any other refusal, such as a missing login.

latest_hit

latest_hit(search: Search) -> Hit | None

The hit with the highest sub-release, or None if there is none.

latest_hits

latest_hits(hits: Iterable[Hit]) -> list[Hit]

One hit per file, the highest sub-release of each, in their first order.

download_url

download_url(archive_url: str, hit: Hit) -> str

The URL to GET with params={"id": search.id}.

check_download

check_download(status_code: int, body: bytes) -> bytes

The ZIP, or :class:EgmsError (401 means the search id expired: search again).

extract_tiff

extract_tiff(archive: bytes, hit: Hit) -> bytes

The mean-velocity GeoTIFF from a tile's ZIP; the CSV is left inside.

extract

extract(archive: Path, suffixes: Iterable[str], directory: Path) -> list[Path]

The files <stem>.<suffix> of a downloaded ZIP, written to directory.

Each is streamed to a .part file and renamed, so a file that exists is whole. :class:EgmsError if the ZIP lacks one of them.