Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
101 commits
Select commit Hold shift + click to select a range
f6d9436
Implement DuckDB-based converter
ivorbosloper Dec 8, 2025
6f52d97
Not sure if this is right
ivorbosloper Dec 8, 2025
0a854ad
Update project
ivorbosloper Dec 8, 2025
996cc38
Fix tests
ivorbosloper Dec 8, 2025
51bea87
Add collection to metadata
ivorbosloper Dec 9, 2025
2d3fc50
Add required to arrow definition
ivorbosloper Dec 9, 2025
852fb6a
Support for sources with different schemas
ivorbosloper Dec 10, 2025
3cf3b8d
Export collection and set compression
m-mohr Dec 10, 2025
c524142
Add caching and warnings
m-mohr Dec 10, 2025
0d5a9f9
Add todos
m-mohr Dec 10, 2025
07d7aac
Fix tests
m-mohr Dec 10, 2025
78fb00d
Implement rewrite to correct GeoParquet
ivorbosloper Dec 10, 2025
dd88ed0
Merge branch 'main' into duckdb_converter
m-mohr Feb 13, 2026
6017ef0
Update fiboa_cli/conversion/duckdb.py
ivorbosloper Feb 13, 2026
993824e
Merge branch 'main' into duckdb_converter
ivorbosloper Feb 16, 2026
920b39d
Update pixi.lock for duckdb
ivorbosloper Feb 16, 2026
5aad9cb
Update pixi
ivorbosloper May 15, 2026
88020f8
Rename es to es_base, so we can introduce a generic Spain-wide converter
ivorbosloper May 15, 2026
42db7be
Converter for Spain (whole), based on the FEGA 2025+ data
ivorbosloper May 15, 2026
25b5777
Merge branch 'main' into ES
ivorbosloper May 19, 2026
892be96
PerFileBaseConverter: per-file migration and merging the result, decr…
ivorbosloper May 19, 2026
06e47ea
Update ES Provinces list
ivorbosloper May 20, 2026
13aa258
Fix hilbert-sort bug
ivorbosloper May 20, 2026
cf85e81
Fail early for debugging
ivorbosloper May 20, 2026
cea4333
Don't upload part files
ivorbosloper May 20, 2026
4ba30ca
Keep geo_parquet version in PerFileConverter
ivorbosloper May 20, 2026
e69174d
Simplify `fiboa publish` for catalog-driven publication
ivorbosloper Aug 21, 2026
f8944b4
NL: new PDOK download location, 2026 concept edition; DE-TH: INSPIRE …
ivorbosloper Aug 22, 2026
3b8d8f1
Declare beautifulsoup4 (used by es_pv, es_vc)
ivorbosloper Aug 22, 2026
d22b603
Drop rows without a required crop:code instead of failing the conversion
ivorbosloper Aug 22, 2026
b92251a
Look up the source column when dropping rows without a crop:code
ivorbosloper Aug 22, 2026
a4afe88
DE-BB: read the shapefile as cp1252
ivorbosloper Aug 22, 2026
53204af
CZ: find the shapefile in nested archive folders
ivorbosloper Aug 22, 2026
33d5ffd
ES-CAT: the 2024 download is a shapefile package
ivorbosloper Aug 22, 2026
4eeff30
SK: row index as id, KODKD block code as block_id
ivorbosloper Aug 22, 2026
077238e
ES-CAT: map the 34 crop names new in the 2024 edition
ivorbosloper Aug 22, 2026
c4466ce
ES-MD: find RECINTO.shp wherever the archive puts it
ivorbosloper Aug 22, 2026
a43baf5
EE: explicit cache names for the WFS responses
ivorbosloper Aug 22, 2026
d5287db
Guard the crop:code row drop (>1% is an error); Europe-LAND: crop_nam…
ivorbosloper Aug 22, 2026
74dd442
ES-CB: determination date from the variant year
ivorbosloper Aug 23, 2026
3a91027
ES-CM: use the year-named service; REST: never cache an error response
ivorbosloper Aug 23, 2026
c84dae8
ES-CM: the id field is OBJECTID_1 (case matters for the page cursor)
ivorbosloper Aug 23, 2026
4870271
ES-AR: per-municipality SIGPAC files listed from IDEAragon
ivorbosloper Aug 23, 2026
07ae56a
ES-AN: CD_USO is the land-use column; determination date from the var…
ivorbosloper Aug 23, 2026
bd45156
REST: drop a page whose download broke off instead of caching it
ivorbosloper Aug 23, 2026
5bfed06
ES-CM: uppercase REST field names, determination date from the varian…
ivorbosloper Aug 23, 2026
047dd0b
ES-CL: the ITACyL server is https-only now
ivorbosloper Aug 23, 2026
0fda9d4
ES-CL: find the shapefiles in the 2025 province subfolders; determina…
ivorbosloper Aug 23, 2026
2a5a3f7
ES-IB: the service now publishes one current-state layer with joined …
ivorbosloper Aug 23, 2026
376a3d7
FR: extract multi-volume 7z archives (py7zr via multivolumefile)
ivorbosloper Aug 23, 2026
da74504
HCAT: absolute ec_mapping_csv URLs also work via commons.ec.load_ec_m…
ivorbosloper Aug 24, 2026
3163614
publish: pass $TMPDIR to tippecanoe (-t), it does not honor the env var
ivorbosloper Aug 24, 2026
fe88bfb
FR: RPG 3.0 (2024) ships the parcels as RPG_Parcelles.gpkg; fix no-op…
ivorbosloper Aug 24, 2026
612ee1e
Merge remote-tracking branch 'origin/duckdb_converter' into publish-p…
ivorbosloper Aug 24, 2026
306dded
JP: default to the 2024 variant (test fixture last), per-feature dete…
ivorbosloper Aug 24, 2026
c1e58b5
Merge remote-tracking branch 'origin/ES2' into publish-portolan
ivorbosloper Aug 24, 2026
9fe304d
Fix es converter test wiring and metadata after ES2 merge
ivorbosloper Aug 24, 2026
b740a07
PerFileConverter: remove part files after a successful merge
ivorbosloper Aug 24, 2026
d26c300
PerFileConverter: vendor the Hilbert helpers missing from released ve…
ivorbosloper Aug 24, 2026
f25e683
FR: backfill-ready variants — 2024 first, explicit PARCELLES targets,…
ivorbosloper Aug 25, 2026
d0f950b
NZ: 2017 and 2020 editions as variants; resolve manual downloads from…
ivorbosloper Aug 25, 2026
8f8d2b4
US CSB: variants 2017-2024 from the single 2017-2024 archive; determi…
ivorbosloper Aug 25, 2026
7d603dd
BE-VLG: the 2026 archive is not named *_GPKG.zip; glob the gpkg inside
ivorbosloper Aug 25, 2026
e112c77
BE-VLG: map the 2026 agpa edition's English column names onto the cla…
ivorbosloper Aug 25, 2026
e117013
Tests for the per-file merge, Hilbert helpers, and command wrapper mo…
ivorbosloper Aug 28, 2026
10e174d
Windows/CI robustness: close parquet readers before delete/replace; l…
ivorbosloper Aug 28, 2026
5463f6c
NL: 2020 edition is a gpkg, and 2009 (zip) exists — fix the variant r…
ivorbosloper Aug 28, 2026
a2b91fc
NL: the 2009-2019 zips contain a FileGDB; extract and glob it
ivorbosloper Aug 28, 2026
a201a98
NL: map the 2009-2019 FileGDB column names; year from the variant
ivorbosloper Aug 28, 2026
b154cf4
Drop rows without an id like rows without a crop code (bounded at 1%)
ivorbosloper Aug 28, 2026
7d5d9aa
DK: the 2008/2009 editions have no crop columns; convert them as boun…
ivorbosloper Aug 28, 2026
10a8cbf
HCAT: skip mapping when an edition has no crop columns at all
ivorbosloper Aug 28, 2026
835cebd
Canonical Hilbert ordering for every output; publish repairs in place
ivorbosloper Aug 28, 2026
6bf0a66
Widen binary/string offsets before the in-place Hilbert take
ivorbosloper Aug 28, 2026
eeceb86
DuckDB converter: declare the appended bbox column as geo covering
ivorbosloper Aug 29, 2026
7243646
Merge branch 'main' into publish-portolan
m-mohr Aug 29, 2026
c5742c0
Adopt vecorel-cli 0.2.16: upstream Hilbert sorting, drop the vendored…
ivorbosloper Aug 29, 2026
4688a00
EE: the 2010-2015 WFS layers are published empty; offer 2016-2024
ivorbosloper Aug 30, 2026
bbee51e
Resort rewrite: preserve schema metadata via batch-wise narrow-schema…
ivorbosloper Aug 30, 2026
5ea3dc1
ES-CB: the REST service now also serves the 2025 SIGPAC layers
ivorbosloper Aug 30, 2026
32171c7
REST converter: discover the qualified key on joined layers; use orde…
ivorbosloper Aug 30, 2026
992f15b
Pre-warm all schema URIs with retries before converting
ivorbosloper Aug 30, 2026
c531e64
Merge branch 'main' into publish-portolan
m-mohr Aug 31, 2026
7147f9f
Fix pixi.lock
m-mohr Aug 31, 2026
6b5a373
REST converter: page by id windows instead of server-side sort
ivorbosloper Aug 31, 2026
db57660
ES-GA: editions 2014-2026, per-campaign column names, determination b…
ivorbosloper Sep 1, 2026
48fe60e
REST converter: key cached pages by service, not just layer id
ivorbosloper Sep 1, 2026
af3bbe8
ES-GA: keep only mapped columns per page
ivorbosloper Sep 1, 2026
b322fa3
Drop empty/missing geometries; widen schema prewarm retries
ivorbosloper Sep 2, 2026
3ce10ae
ES-GA: exclude PR (pasto arbustivo) before the 2023 campaign
ivorbosloper Sep 2, 2026
bb2e0bc
PT: add the 2025 edition, which changed shape
ivorbosloper Sep 3, 2026
f9b6d53
ES-GA, ES-CM: give both converters their region in short_name
ivorbosloper Sep 3, 2026
16fc5b3
ES-CB: name Cantabria as the provider
ivorbosloper Sep 3, 2026
715afa2
ES-IB: point the provider at the Balearic government, not Navarra's
ivorbosloper Sep 3, 2026
e6048e6
EC-EE: name the shapefile inside the archive
ivorbosloper Sep 3, 2026
6e78b99
EC-LV, EC-SI: stop requiring columns the source leaves empty
ivorbosloper Sep 3, 2026
c3f89ad
EC-EE: migrate the year column the dataset actually has
ivorbosloper Sep 3, 2026
2105e43
EC-SI, EC-EE: require only what every parcel actually has
ivorbosloper Sep 3, 2026
d15e0d1
AT: extract the archive instead of reading the GeoPackage through the…
ivorbosloper Sep 3, 2026
9d8a640
Hilbert order: fall back to the data's extent when the CRS has no are…
ivorbosloper Sep 3, 2026
e972a2d
DE-NDS: give the collection an id
ivorbosloper Sep 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 22 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,36 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).

## [Unreleased]

-
- Add DuckDB BaseConverter for efficiently transforming large datasets
- Fix `use_variant_as_determination`: the determination:datetime column was dropped again because it was not listed in `columns` (affected DK, HR)
- Declare the beautifulsoup4 dependency that the ES-PV and ES-VC converters import
- EE: name the cached WFS responses (ee_gsaa_<year>.gml)
- ES-MD: the archive no longer nests RECINTO.shp in a folder
- SK: KODKD is the (non-unique, sometimes empty) LPIS block code, keep it as block_id and use the row index as id
- ES-CM: always read the year-named SIGPAC service (the unnamed one moved on to 2025 with a different id field)
- REST converters: do not keep an error response as a cached page
- ES-CB: determination date from the variant year (was an empty string, which broke the STAC temporal extent)
- ES-CAT: the 2024 download is a shapefile package, not a GeoPackage; 34 crop names new in 2024 added to the mapping
- CZ: find the shapefile in nested archive folders (2026)
- DE-BB: read the shapefile as cp1252 (its .cpg wrongly says UTF-8)
- NL: new PDOK download location (rvo/gewaspercelen/atom), add the 2026 concept edition
- DE-TH: note the INSPIRE download service
- Drop rows without a crop:code (required by the crop extension) with a warning instead of failing the conversion (BE-VLG 2023, ES-CN had one such row each); more than 1% missing is an error
- Europe-LAND converters: use crop_name as crop:code when the file's crop_code column is empty (LT 2024)
- BE-VLG: derive determination:datetime from the variant year instead of a constant date
- `fiboa publish` no longer uploads to S3 or generates README/LICENSE files. It creates GeoParquet, PMTiles and a STAC Collection with relative links, `file:size`/`file:checksum` and a web-map-links v1.3.0 `pmtiles` link. Publishing is done by catalogs such as the [harmonized field data catalog](https://github.com/fieldsoftheworld/harmonized-field-data-catalog).
- Add Italy Tuscany (IT-1) basd on EuroCrops v2
- Suuport multiple years for CZ
- Multiple years for DE_sh
- Multiple year support for HR
- Introduce FiboaBaseConverter.use_variant_as_determination for setting proper determination_date
- Update years for DK (2025, 2026)
- Converter for Spain (whole), based on the FEGA 2025+ data
- Update fr-converter to support 2021/2022 files
- Converter for Bavaria, Germany LPIS field blocks (de_by_block)
- Converter for Hesse, Germany LPIS reference parcels
- PerFileBaseConverter: per-file migration and merging the result, decreasing memory requirements for large data sets
- Update vecorel-cli to v0.2.16:
- Converter output is sorted by Hilbert distance
- Commands exit with a non-zero exit code when they report a failure
Expand All @@ -32,7 +52,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- Fix the column additions of the determination fields in the AI4SF converter
- Add HCAT to datasets where possible
- Updated years & variants for at_crop, be_vlg, es_an, es_cl, es_pv, ie, pt, se
- Extend create_stac, include include fiboa data
- Extend create_stac, include fiboa data
- Publish command; skip hidden files, generate better texts
- Fix to vecorel: converter.license and provider should be string
- Added a Dockerfile to simplify working with fiboa
Expand Down
47 changes: 22 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ fiboa CLI supports various commands to work with the files:
- [Improve a fiboa Parquet file](#improve-a-fiboa-parquet-file)
- [Update an extension template with new names](#update-an-extension-template-with-new-names)
- [Converter for existing datasets](#converter-for-existing-datasets)
- [Publish datasets to source coop or your own s3 repository](#publish-datasets-to-source-coop-or-your-own-s3-repository)
- [Publish datasets](#publish-datasets)
- [Development](#development)
- [Implement a converter](#implement-a-converter)
- [Run in Docker](#run-in-docker)
Expand Down Expand Up @@ -193,46 +193,43 @@ Use any of the IDs from the list to convert an existing dataset to fiboa:

See [Implement a converter](#implement-a-converter) for details about how to

### Publish datasets to source coop or your own s3 repository
### Publish datasets

`fiboa publish <dataset> -o <target>`

The publish converts and publishes a fiboa dataset to source coop or your own s3 repository. The target directory
will be filled with the following files:
Converts and validates a fiboa dataset and prepares everything that is needed to publish it
in a (STAC-based) catalog. The target directory will be filled with the following files:

```
<target>/
<dataset>.parquet
<dataset>.pmtiles # requires working ogr2ogr and tippecanoe
stac/collection.json
README.md # generated if --generate-meta/-gm flag is present
LICENSE.txt # generated if --generate-meta/-gm flag is present
<dataset>[-<variant>].parquet
<dataset>[-<variant>].pmtiles # requires working ogr2ogr and tippecanoe
collection.json # STAC Collection with relative links to the files above
```

This directory is synchronized to the s3 repository (default source.coop/fiboa/data).
The STAC Collection carries `file:size` and `file:checksum` for the files and a
`pmtiles` link (web-map-links extension). Existing files in the target directory are reused,
delete them to regenerate. Uploading to a bucket and catalog-specific metadata
(README, styles, thumbnails, ...) are the job of the catalog that publishes the data, e.g. the
[harmonized field data catalog](https://github.com/fieldsoftheworld/harmonized-field-data-catalog).

**Requirements**: Requires the [aws CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html) to be installed,
and `AWS_ACCESS_KEY_ID` with `AWS_SECRET_ACCESS_KEY` environment variables. Also, for generating the pmtiles file,
it requires [ogr2ogr](https://gdal.org/programs/ogr2ogr.html) and [tippecanoe](https://github.com/mapbox/tippecanoe).
The command runs:

The command executes the following steps:

- `fiboa convert` to generate a fiboa parquet dataset. All convert parameters are passed to the converter.
- `fiboa validate` to validate the fiboa dataset
- creates a <dataset>.pmtiles from the parquet file. Uses ogr2ogr and tippecanoe
- `fiboa create-stac-collection` to create a STAC collection
- `fiboa publish` to publish the fiboa dataset to a source coop or your own s3 repository
- `fiboa convert` to create a `<dataset>[-<variant>].parquet` file
- `fiboa validate` to validate the GeoParquet file
- `ogr2ogr | tippecanoe` to create the PMTiles file
- `fiboa create-stac-collection` to create the STAC Collection

Examples:

- `fiboa publish at_crop -o data/at_crop`
- `fiboa publish -c /tmp/cache -gm br_conab -o data/br_conab`
- `fiboa publish at -o data/at`
- `fiboa publish -c /tmp/cache nl --variant 2025 -o data/nl/2025`

Relevant parameters:

- `--generate-meta/-gm` Generatse the README.md and LICENSE.txt files if absent, based on data-survey and converter properties.
- `--data-url` The URL to the data repository, used when generating the README
- `--s3-upload-path` The `aws s3 sync` target. Defaults to `s3://source.coop/fiboa/data` . Uploading requires the `aws` CLI, and `AWS_ACCESS_KEY_ID` with `AWS_SECRET_ACCESS_KEY` environment variables.
- `--variant` Choose the variant (e.g. year) of a dataset, defaults to the first variant.
- `--no-pmtiles` Skip PMTiles generation.
- `--tippecanoe-opts` Options passed to tippecanoe, defaults to `-zg --drop-densest-as-needed --extend-zooms-if-still-dropping`.

Check `fiboa publish --help` for more details.

Expand Down
5 changes: 5 additions & 0 deletions build/lib/fiboa_cli/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from vecorel_cli.registry import Registry

from .registry import FiboaRegistry

Registry.instance = FiboaRegistry()
Empty file.
3 changes: 3 additions & 0 deletions build/lib/fiboa_cli/cli/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from vecorel_cli.cli.setup import setup_cli

run = setup_cli()
Empty file.
34 changes: 34 additions & 0 deletions build/lib/fiboa_cli/conversion/convert_gml.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import os

import geopandas
from loguru import logger


def gml_assure_columns(data, path, uri, layer, **kwargs):
# if GDAL opens a GML file, it generates a gfs file in which it tries to 'guess' a
# mapping from the GML XML file to features. This is not always correct.
# Call this function to add additional attributes from the GML
# We modify the GFS file, a more elegant solution is preferred
# See https://gdal.org/en/latest/drivers/vector/gml.html#schema for more info

if next(iter(kwargs)) not in data.columns:
logger.info("Patching generated GFS file")
assert path.endswith(".gml"), "Expected a gml file"
gfs_file = path[:-4] + ".gfs"
assert os.path.exists(gfs_file), "Expected a local, generated GFS file by OGR-GML driver"
# Fix GFS template file
with open(gfs_file, mode="r") as file:
gfs_xml = file.read()

for property in kwargs:
assert f"<Name>{property}</Name>" not in gfs_xml, "Expected unpatched gfs file"

lines = gfs_xml.splitlines()
for property, elements in kwargs.items():
element_str = "\n".join(f"<{k}>{v}</{k}>" for k, v in elements.items())
lines.insert(-2, f"<PropertyDefn><Name>{property}</Name>{element_str}</PropertyDefn>")
with open(gfs_file, mode="w") as file:
file.write("\n".join(lines))

data = geopandas.read_file(path, layer=layer)
return data
100 changes: 100 additions & 0 deletions build/lib/fiboa_cli/conversion/converter_rest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import os
from urllib.parse import urlencode

import geopandas as gpd
import requests
from vecorel_cli.vecorel.util import get_fs, stream_file


class EsriRESTConverterMixin:
cache_folder = None
rest_base_url = None
rest_params = {}
rest_attribute = "OBJECTID" # orderable, filterable, indexed

def rest_layer_filter(self, layers):
return next(iter(layers))

def get_urls(self):
assert self.rest_base_url, (
"Either define {c}.rest_base_url or override {c}.get_urls()".format(
c=self.__class__.__name__
)
)
return {"REST": self.rest_base_url}

def download_files(self, uris, cache_folder=None):
# Read-data will just stream alle pages of rest-service
if next(iter(uris), "").startswith("REST"):
self.cache_folder = cache_folder
return list(uris.values())

# This happens when input_file param is used
return super().download_files(uris, cache_folder)

def get_data(self, paths, **kwargs):
if isinstance(paths[0], tuple):
# (path, uri) pairs from the base downloader: input_file param was used
yield from super().get_data(paths, **kwargs)
return

base_url = paths[0] # loop over paths to support more than 1 source
source_fs = get_fs(base_url)
cache_fs, cache_folder = self.get_cache(self.cache_folder)

service_metadata = requests.get(base_url, {"f": "pjson"}).json()
layer = self.rest_layer_filter(service_metadata["layers"])
page_size = service_metadata["maxRecordCount"]
layer_url = f"{base_url}/{layer['id']}/query"
get_dict = self.rest_params | {
"outFields": "*",
"returnGeometry": "true",
"f": "geojson",
"sortBy": self.rest_attribute,
"resultRecordCount": page_size,
}
gdfs = []
last_id = -1
while True:
get_dict["where"] = f"{self.rest_attribute}>{last_id}"
url = f"{layer_url}?{urlencode(get_dict)}"
if cache_fs is not None:
cache_file = os.path.join(
cache_folder, f"{self.id}_{layer['id']}_{last_id}.geojson"
)
if not cache_fs.exists(cache_file):
try:
with cache_fs.open(cache_file, mode="wb") as file:
stream_file(source_fs, url, file)
except Exception:
# A download that broke off must not survive as a cached page
if cache_fs.exists(cache_file):
cache_fs.rm(cache_file)
raise
url = cache_file

try:
data = gpd.read_file(url)
except Exception as e:
# An error response from the server must not survive as a cached page
if cache_fs is not None and cache_fs.exists(url):
cache_fs.rm(url)
raise RuntimeError(f"Could not read page {len(gdfs)} of {layer_url}: {e}") from e
print(
f"Read {len(data)} features, page {len(gdfs)} from [{data.iloc[0, 0]} ... {data.iloc[-1, 0]}]"
)
# joined layers return the field as <table>.<name>
id_column = next(
(
c
for c in data.columns
if c == self.rest_attribute or c.endswith("." + self.rest_attribute)
),
self.rest_attribute,
)
last_id = data[id_column].values[-1]

yield data, base_url, base_url, layer["id"]

if not len(data) >= page_size:
break
Loading