REST converters: page by id windows - #232
ivorbosloper wants to merge 8 commits into
Conversation
090f4d7 to
400191f
Compare
Paging by `sortBy` + `attribute > last_id` asks the server to sort the whole layer for every page. On Cantabria's joined SIGPAC layers that costs about 100 seconds per request — days for one edition — where a range filter on the same indexed key answers in one or two. Galicia is 15.8 million recintos per edition, in pages of 1000. Paging by half-open id windows removes the sort: the key is unique, so a window of `page_size` ids cannot overflow a page, and gaps only produce empty windows, which are skipped. Pages cached by the old scheme are keyed by the previous page's last id; on a dense layer those coincide exactly with a window, so one is reused when its own ids prove it covers the window completely. Three more things this turned up, each invisible in the output: - **Layer ids repeat across services.** Every SIXPAC_<year> MapServer has its Recintos layer at id 2, so a cache keyed on the layer alone served one year's pages for another. The service is part of the key now. - **An error response was cached as if it were data.** Esri answers a failed query with 200 and a JSON error body; that body was written to the cache file and every later run read it back. So was a download that broke off halfway. Neither survives now. - **Joined layers qualify every field with its table name**, so "OBJECTID" matches nothing and the paging filter silently returns everything. The real key field is discovered from a one-row probe before paging starts. `rest_format`, the fixture-reading branch and the way `rest_params["where"]` is combined with the paging filter are unchanged; the test that pins them is updated for the new clause, which now reads `OBJECTID>0 AND OBJECTID<=1000 AND (<the converter's own filter>)`. tests/test_converter_rest.py serves a small fake service of five features in pages of two, which is enough to hold every one of these: the window arithmetic, the cache naming, the two guards, the qualified key, and the reuse of a legacy page only when its ids cover the window. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DVx9uQV2QPM8ecPAY3ZjXG
400191f to
07aafe9
Compare
9271232 to
8170edb
Compare
Co-authored-by: Matthias Mohr <m.mohr@moregeo.it> (cherry picked from commit 090f4d7)
…ervice Three things every REST converter needs, each of which es_ib or es_cb had a copy of: - a joined layer qualifies every field (SIGPAC_FOGAIBA.DN_OID), so a converter's `columns` match nothing; the prefixes are stripped on the way out of get_data, first table winning, which is the one with the geometry - an edition may live in a service of its own, so a variant whose value is a URL names the service to read it from - the id-bound query is the one sorted query left, and the Balearic proxy answers two in three with a 502; it is retried five times Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DVx9uQV2QPM8ecPAY3ZjXG
8170edb to
cc57c18
Compare
The yearly layers moved out of GOIB_SIGPAC_IB into GOIB_SIGPAC_HISTORIC_IB, which keeps 2022-2025; what is left in the first service is one current-state layer, ahead of the historic newest (maig against gener 2026). Each variant names the service it comes from. The years before 2022 are withdrawn. Both services join their parcels to the municipality and land-use tables, so their fields arrive table-qualified; the mixin strips that. The snapshot month they carry becomes determination:datetime through column_migrations — the current service writes "maig 2026", the historic ones "Febrer2024.0". `provider` also pointed at Navarra, a copy-paste between the Spanish converters. The converter had no test. It has one page of each service now, 100 features as the service answers them, so the qualified names and both spellings of the snapshot month are covered. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DVx9uQV2QPM8ecPAY3ZjXG
The converter published one edition under a licence Cantabria does not use. It claimed CC-BY-NC-4.0, justified by comments pointing at caib.es — the Balearic Islands, a copy-paste from the es_ib converter. Cantabria licenses its cartography under Decreto 87/2013 (modified by Decreto 102/2018), which defines two licences of its own, both free of charge: a non-commercial one and a commercial one, the latter needed only for reselling the data. So both the Creative Commons label and the NC restriction were wrong. The attribution is now the wording the licence prescribes, verbatim in Spanish, and the province is named as the provider — the field was empty, so the collection had no producer at all. On the data: the service serves SIGPAC layers from 2010, and the 2025 layers appeared while this was being worked on. Editions 2010-2014 are joined layers whose fields arrive table-qualified (SIGPAC_2014_RECFE_ETRS89.PROVINCIA), so the prefixes are stripped, first occurrence winning because the geometry table leads. Each edition's determination date comes from its variant year, where it used to be an empty string that broke the STAC temporal extent. Sixteen editions are built and published, ~600k filtered fields each. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DVx9uQV2QPM8ecPAY3ZjXG
ES-IB: editions 2022-2026, and the right provider
ES-CB: editions 2010-2025, and the right licence
There was a problem hiding this comment.
🟡 Changes recommended
Cache provenance remains unsafe, and the Balearic server-side filter still uses an unqualified joined-layer field.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
Reworks Esri REST paging for faster, safer conversion and expands Balearic/Cantabrian edition support.
Changes:
- Pages REST layers using ID windows with service-aware caching and error cleanup.
- Handles qualified joined-layer fields and URL-backed variants.
- Adds extensive REST and ES-IB conversion coverage.
File summaries
| File | Description |
|---|---|
fiboa_cli/conversion/converter_rest.py |
Implements window paging, cache handling, retries, and field normalization. |
fiboa_cli/datasets/es_ib.py |
Adds current/historic services and snapshot-date handling. |
fiboa_cli/datasets/es_cb.py |
Updates editions, licensing, and determination dates. |
tests/test_converter_rest.py |
Covers REST paging and caching behavior. |
tests/test_converters.py |
Updates REST parameter assertions. |
tests/test_convert.py |
Tests current and historic ES-IB variants. |
tests/data-files/convert/es_ib/es_ib_2024.geojson |
Provides historic joined-layer test data. |
CHANGELOG.md |
Documents converter changes. |
Review details
Suppressed comments (1)
fiboa_cli/conversion/converter_rest.py:135
- The new cache key still omits the effective filter/variant. For example,
DESTConverterchangesrest_params['where']per edition while using the same service and layer (fiboa_cli/datasets/de_st.py:54-61), so a matching ID window cached for one edition can be returned for another. Include a stable fingerprint of the effective query (at least the variant andrest_params) in the cache key.
cache_file = os.path.join(
cache_folder,
f"{self.id}_{service}_{layer['id']}_r{lo}.{self.rest_format}",
)
- Files reviewed: 7/9 changed files
- Comments generated: 3
- Review effort level: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| data = self._window_from_legacy_cache( | ||
| cache_fs, cache_folder, layer["id"], lo, hi, page_size | ||
| ) |
There was a problem hiding this comment.
The mechanism is right: the legacy name is {id}_{layer}_{key}, so it carries the dataset and the layer but not the service or the variant, and the check only proves the id range. Two services that number their layers the same way would be indistinguishable — which is exactly the case this PR's own comment cites, every SIXPAC_<year> MapServer serving Recintos at layer 2.
What saves it today is an accident of the one dataset that has such pages. On the machine that builds the catalog there are 9,306 legacy pages, all es_cb, and Cantabria gives every edition its own layer id — 3, 7, 11, 15, 19 … 71 — so a page belongs to exactly one edition. es_ib, es_ga and es_cm have none at all.
An accident is not a guarantee, and the saving is bounded: those 9,306 pages are one dataset's, against 210,053 already cached under the new key. I would rather drop the reuse than keep a path that cannot prove where its data came from. Alternatively it could be restricted to converters without variants, where the ambiguity cannot arise — say which you prefer and I will make the change.
| rest_params = { | ||
| "where": "USO_SIGPAC NOT IN ('AG','CA','ED','FO','IM','IS','IV','TH','ZC','ZU','ZV','MT')" | ||
| } |
There was a problem hiding this comment.
The server resolves the unqualified name. Checked against the published editions, counting rows carrying any of the twelve excluded codes:
2022: 478,041 rows, 0 with an excluded use code
2026: 349,087 rows, 0 with an excluded use code
If the predicate were rejected the request would fail, and if it were ignored those codes would be there — AG, ED and ZU are common SIGPAC uses, so zero out of 349,087 is not chance. ArcGIS accepts the bare field name in where on these joined layers even though it answers with the qualified one.
| if not any("." in c for c in gdf.columns): | ||
| return gdf | ||
| renames = {} | ||
| for column in gdf.columns: | ||
| name = column.rsplit(".", 1)[-1] | ||
| if name not in gdf.columns and name not in renames.values(): | ||
| renames[column] = name | ||
| gdf = gdf.rename(columns=renames) | ||
| return gdf.loc[:, ~gdf.columns.duplicated()] |
There was a problem hiding this comment.
Right about the mechanism, wrong about the consequence. With A.USO and B.USO and no bare USO, the first is renamed and the second is skipped because the suffix is already a value in renames, so columns.duplicated() sees two different names and keeps both — B.USO does stay qualified.
It does not reach the output, because a column no columns entry maps is dropped before the file is written. So the documented behaviour holds — the first table wins — and the leftover is discarded rather than published with its prefix.
Happy to make it explicit by dropping the remaining qualified columns instead of leaving them to be swept up later, if you would rather the function finish its own job.
Paging by
sortBy+attribute > last_idsorts the whole layer per page: ~100 s per request on Cantabria's joined layers, and Galicia is 15.8M recintos per edition. Half-open id windows remove the sort, and pages cached by the old scheme are reused where their ids prove they cover a window.Three bugs found on the way, none visible in the output:
SIXPAC_<year>has Recintos at id 2, so one year's cache served anotherRECINTOS.OBJECTID, so the paging filter matched nothing and returned everythingA second commit adds three things the Balearic services need, each of which a converter had a copy of: a joined layer's table prefixes are stripped for every converter (so
es_ibandes_cblose theirs), a variant whose value is a URL names the service that edition lives in, and the id-bound query — the one sorted query left — is retried, because that proxy answers two in three with a 502.Not a copy: main's
rest_formatandwhere-clause work is kept.tests/test_converter_rest.pyis new; mixin coverage 19% → 94%.