GeoCase
Two pixels out of a hundred are NoData, and your mean elevation is off by 200 metres.
Here is the code. It looks fine, it passes review, and it is wrong:
def mean_elevation(array):
return float(array.mean())
Point it at a GeoCase case that ships with the package:
import pytest
@pytest.mark.geocase_case("geotiff_nodata_small")
def test_mean_elevation_ignores_nodata(geocase_case):
array, _profile, nodata = geocase_case.read(1)
valid = array[array != nodata]
assert mean_elevation(array) == pytest.approx(float(valid.mean()), rel=0.01)
E assert -152.8628387451172 == 48.07874298095703 ± 0.480787
The file carries an explicit -9999 NoData sentinel in 2 of its 100 pixels. array.mean()
averages the sentinel in and reports −152.9 m for terrain whose real mean is 48.1 m.
Nothing raises and nothing warns — the number is simply wrong, and it stays wrong all the way
into the report. The only thing that catches it is a test file that actually has NoData in it.
That is what GeoCase is: 174 curated vector, raster and NetCDF files, each one built around a failure mode that survives code review.
Most of them are about geometry, CRS and georeferencing conventions — the assumptions a codebase makes about where a pixel or a vertex actually is:
- a rotated geotransform, where the shortcut inverse matrix everyone writes is wrong, and wrong quietly;
- a bottom-up raster with a positive
eterm, read upside down without a single warning; - pixel-is-area versus pixel-is-point anchoring, a half-pixel shift that survives review;
- geometry crossing the antimeridian and coming back as a ring around the globe, or a footprint reaching lon 180.22 that sends a tile request to a tile that does not exist;
- a CRS mismatch between two layers that overlay perfectly on screen;
- EPSG axis order flipping latitude and longitude;
- NoData averaged into a statistic, as above.
Radiometric conventions for Sentinel-1 and Sentinel-2 — scale factors, BOA offsets, dB
conversion — are one vertical inside that, machine-checked against
geofacts. They are not the thesis.
If your codebase moves pixels and geometry around and reads them with plain GDAL, you are the audience this corpus serves best.
Works with plain GDAL
case.primary_path is an ordinary filesystem path. gdal.Open just works, and the base install
needs only pydantic, pyyaml and geofacts — no rasterio, no geopandas, no xarray:
import geocase
from osgeo import gdal
case = geocase.load_case("rotated_two_islands")
ds = gdal.Open(str(case.primary_path))
print(ds.GetGeoTransform())
The extras exist for the convenience loaders (case.load(), case.read()), not for reading the
files. Enumerating, selecting and resolving cases needs no optional dependency at all.
The georeferencing-conventions suite is the shortest path from install to a real finding:
@pytest.mark.geocase_suite("georeferencing-conventions")
def test_georeferencing(geocase) -> None:
...
The realistic alternative is not "no tests" — it is the test_data/sample.tif someone exported
once and the numpy arrays each test improvises. Those pass because they were chosen by the same
person who wrote the code, so they encode the same assumptions. GeoCase cases were not, and they
ship as a versioned dependency instead of a folder nobody remembers the provenance of. (They also
give a coding assistant something to reach for instead of inventing a fixture that agrees with
the bug.)
Status: 1.0.0 is available on PyPI —
pip install geocase. The compatibility promise covers two surfaces — thepytestworkflow (fixtures and markers) and theimport geocasepublic API. 174 bundled cases, 5.1 MB. Remote dataset transport is deferred to v1.1; see the changelog.
Quick Start
1) Install
For local development in this repo:
pip install -e ".[dev]"
There are two supported install shapes, and picking the right one matters.
Greenfield — no geospatial stack yet, let pip build one:
pip install "geocase[all]"
You already have a working geo stack — GDAL, geopandas, rasterio, whether from conda,
system packages or a --system-site-packages venv:
pip install geocase
Plain geocase is enough to enumerate, select and resolve every case, because primary_path
is just a path and you already have the readers. Use it. [all] re-resolves numpy and pandas
and can shadow or break a working stack — it is for greenfield environments only.
geocase.raster, the in-memory raster fixture builder, needs numpy. If your stack has no
numpy, add the array extra: pip install "geocase[array]".
A conda-forge feedstock does not exist yet. When one does, installing from it will not carry the extras, since bundling GDAL would make the conda package far heavier than the PyPI equivalent:
conda install -c conda-forge geocase
GeoCase depends on geofacts at
runtime — a zero-dependency table of geospatial product facts (radiometric
constants, CRS conventions) that the raster presets are machine-checked
against. Everything else is optional and gated behind extras.
2) Write a test with GeoCase markers
import pytest
@pytest.mark.geocase_case("dateline_crossing_polygon")
def test_vector_case_loads(geocase_case) -> None:
gdf = geocase_case.load()
assert geocase_case.id == "dateline_crossing_polygon"
assert gdf.crs is not None
@pytest.mark.geocase_select(category="raster")
def test_all_raster_cases_have_pixels(geocase) -> None:
data, _, _ = geocase.read(1)
assert data.size > 0
3) Run tests
pytest -v
Run only GeoCase-marked tests:
pytest -m "geocase_case or geocase_suite or geocase_select" -v
CI Jobs
This repository uses GitHub Actions, defined in .github/workflows/.
ci.yml runs on pushes to main and on pull requests:
catalog— catalog integrity checks (build_case_index.pysmoke check,validate_catalog.py, fixture and checksum gates, generated-page freshness)tests— the wholetests/directory on Python 3.11 and 3.14, reporting coverage (not gated)floor— the suite with no extras at the lowest declared core versions on Python 3.11lint—ruff format --checkandruff checkoversrcandteststypecheck—mypy srcdocs—mkdocs build --strict
release.yml runs only on vX.Y.Z tags; see
Releasing.
Local equivalents:
python scripts/build_case_index.py --check
python scripts/validate_catalog.py
python -m pytest tests -q
ruff format --check src tests && ruff check src tests
Core Concepts
@pytest.mark.geocase_case(...): select explicit case IDs.@pytest.mark.geocase_suite(...): use named suites.@pytest.mark.geocase_select(...): select by metadata (category,format,geometry_type,tags,risk_types_any, etc.).geocase: auto-parameterized fixture (one invocation per resolved case).geocase_case: convenience fixture for exactly one resolved case.- CLI tooling is optional; the primary workflow is plain
pytest.
If a GeoCase marker is missing, resolves no cases, refers to an unknown suite, or geocase_case resolves more than one case, the plugin now raises a focused pytest.UsageError that explains what to fix.
Learn More
docs/getting-started.mddocs/testing-your-function-with-geocase.mddocs/case-discovery.mddocs/assertions-reference.mddocs/examples-index.mddocs/benchmark/quickstart.md— the LLM benchmark built on the catalog. Experimental: not part of the v1.0 compatibility promise and not published to the docs site.src/geocase/raster/—geocase.raster, a raster primitive needing only numpy (thearrayextra) plus Sentinel-1/2 presetsdocs/plans/development-plan.mddocs/contributing/workflow.mddocs/design/case-recommendation-service.md
Metadata
Release files for geocase 1.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| geocase-1.1.0.tar.gz | 1.2 MB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| geocase-1.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 2.6 MB
Release files / geocase-1.1.0.tar.gz
| Download URL | geocase-1.1.0.tar.gz |
|---|---|
| Size | 1.2 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
af1db60ab44800cf6c3530f9d2d58f3431ee7224d30a2ff5cf212c3bcccb4f54
|
|
BLAKE2b-256 checksum How to use checksums |
4c74a26322161d9bc781ba592ed9a19545d815a8b80b281c74684492ca500988
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 27, 2026.
Transparency logRelease files / geocase-1.1.0-py3-none-any.whl
| Download URL | geocase-1.1.0-py3-none-any.whl |
|---|---|
| Size | 1.4 MB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
a7669d6fe2694f6d563b10af7649d474571c54296cc1d06f174d516568f09fc3
|
|
BLAKE2b-256 checksum How to use checksums |
f254de2fc7e981181abf836d119a919b61849345c7733f74fc229b24d8b9562c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 27, 2026.
Transparency log