Structure and Planning¶
Status (August 2026): Core implementation is complete;
pytest tests -qis green at 780 passed, 1 skipped, and the public API is pinned for v1.0. See workflow.md for the detailed status tracker and development-plan.md for the roadmap, which is authoritative on scope.
The core implementation is complete:
- ~~implement the metadata models~~ ✅
- ~~implement the YAML loaders~~ ✅
- ~~implement the registry~~ ✅
- ~~implement selectors~~ ✅
- ~~implement suites~~ ✅
- ~~implement case factory + loaders~~ ✅
- ~~add a few tests~~ ✅ (781 collected)
- ~~wire the pytest plugin~~ ✅
Remaining work focuses on documentation cleanup, additional test cases, and polish.
See development-plan.md for the current roadmap.
1. What the project is becoming¶
GeoCase is now basically this:
- a catalog of cases
- each case has metadata
- metadata is loaded into Python objects
- those objects can be filtered and grouped into suites
- selected cases are then used in
pytest.mark.parametrize(...)
So the core flow is:
case.yaml -> Pydantic model -> registry -> selectors/suites -> case object -> load() -> pytest
That is the mental model to keep in mind while coding.
2. Folder structure explained briefly¶
Below is the meaning of the main folders and why they exist.
Root files¶
README.md¶
Project entry point. Explains what GeoCase is, why it exists, and shows a minimal example.
pyproject.toml¶
Package configuration. Declares dependencies, optional extras, build system, pytest plugin entry point, linting config, etc.
LICENSE¶
Open-source license.
.gitignore¶
Prevents committing generated files, caches, virtualenvs, downloads, and temp artifacts.
mkdocs.yml¶
Documentation site configuration if you publish docs with MkDocs.
docs/¶
Human documentation.
docs/index.md¶
Landing page for docs.
docs/getting-started.md¶
Basic install and first use.
docs/philosophy.md¶
Explains the design principles: cases over random files, metadata-driven parameterization, small bundled core, remote later.
docs/using-parameterized-tests.md¶
Shows the main developer workflow with pytest.
docs/adding-a-case.md¶
Instructions for contributors to add new cases correctly.
docs/remote-datasets.md¶
Explains bundled vs remote vs private storage.
docs/design/database-design.md¶
Explains the longer-term catalog backend idea. Kept in the repository but not published to the site: it designs a backend that does not exist.
Why this folder exists: because GeoCase is partly a toolkit and partly a convention. Good docs are part of the product.
src/geocase/¶
Main Python package.
__init__.py¶
Public API surface. Re-export the small number of things users should import directly, like:
get_caselist_casesselect_casessuite
py.typed¶
Tells type checkers that the package is typed.
src/geocase/api/¶
Optional but useful if you want a clean public-facing layer.
api/public.py¶
Thin stable wrapper around internal modules. This is where you define the functions you want users to call.
api/types.py¶
Shared public-facing type aliases or protocols if you want to keep internals separate.
Why this folder exists: to stop users from importing deep internal modules directly.
For v0.1 this folder is optional, but it is a clean design choice.
src/geocase/catalog/¶
This is the real core.
catalog/models.py ✅¶
Defines Pydantic v2 models:
CaseMetadata— full case description with strict validationSuiteMetadata— suite definition with selection criteriaSuiteSelection— filter fields using Literal types (Category,TestTier, etc.)FileMap,RemoteInfo,SourceInfo,AssertionHints— supporting models
Type aliases (Category, FormatType, TestTier, SizeClass, StorageClass, LoaderHint, Status) are defined as Literal types and reused throughout.
Why it exists: all metadata needs one clear runtime representation.
catalog/loader.py ✅¶
Reads YAML files and converts them into Pydantic models.
Implemented functions:
load_case_metadata(path) -> CaseMetadataload_suite_metadata(path) -> SuiteMetadataload_case_index(path) -> list[str]load_suite_index(path) -> list[str]
Why it exists: separates file parsing from the rest of the logic.
catalog/registry.py¶
Loads the full catalog from case-index.yaml, stores it in memory, and provides lookup functions.
Why it exists: this is the single source of truth for available cases.
catalog/selectors.py¶
Filters cases by metadata, such as:
- category
- tag
- risk type
- test tier
- format
- storage class
Why it exists: selectors are what make parameterized testing elegant.
catalog/suites.py¶
Loads suite definitions and resolves them into case lists.
Why it exists: named suites are more convenient than repeating selectors everywhere.
scripts/validate_catalog.py¶
Extra validation logic beyond Pydantic, such as:
- referenced files exist
- remote info is present for remote cases
- duplicate case IDs do not exist
- suite references are valid
- declared
size_classmatches the payload's actual size
Why it exists: schema validation alone is not enough.
This lives in scripts/, not in the package. catalog/validators.py was reserved for it
and stayed a one-line docstring that nothing imported, so it was deleted in Batch 3;
plan 03 had already decided validation stays in scripts/.
catalog/manifests.py¶
Handles remote manifest files for downloadable extended datasets.
Why it exists: keeps remote logic separate from core bundled metadata.
See also: manifests-and-storage.md for the
distinction between manifest support, storage support, checksums, and why both
layers are needed.
src/geocase/cases/¶
Case objects and category-specific behavior.
cases/base.py¶
Defines the common base class, e.g. BaseCase.
Why it exists: every case shares metadata, root path, id, and file resolution logic.
cases/vector.py¶
Defines VectorCase and load() using GeoPandas.
cases/raster.py¶
Defines RasterCase and open() using Rasterio.
cases/netcdf.py¶
Defines NetCDFCase and load() using Xarray.
cases/factory.py¶
Takes metadata and returns the right case class.
Why it exists: users should not have to care which subclass to instantiate.
src/geocase/loaders/¶
Actual file loading utilities.
loaders/geopandas_loader.py¶
Reads vector files.
loaders/rasterio_loader.py¶
Opens raster files.
loaders/xarray_loader.py¶
Opens NetCDF-like cases.
loaders/generic.py¶
Fallback logic for generic files or untyped loading.
Why this folder exists: keeps case objects small and prevents loader code from being duplicated.
For v0.1, you can inline loading inside cases/ and move it here later. But long-term this separation is cleaner.
src/geocase/assertions/¶
Reusable test helpers.
assertions/geometry.py¶
Examples:
assert_not_empty_geodataframeassert_all_geometries_validassert_geometry_types
assertions/crs.py¶
Examples:
assert_has_crsassert_epsg
assertions/raster.py¶
Examples:
assert_raster_has_crsassert_raster_has_nodata
assertions/topology.py¶
Topology-specific checks later.
assertions/metadata.py¶
Checks on case metadata or expected properties.
Why this folder exists: GeoCase should provide not only data, but also reusable testing primitives.
src/geocase/storage/¶
storage/hashing.py ✅¶
SHA-256 and byte-size verification. scripts/generate_checksums.py imports
sha256_file from here, so bundled fixtures and remote artifacts are hashed by the
same code.
Why it exists: local and remote cases should still feel like one catalog.
Transport is deferred to v1.1. local.py, remote.py, and cache.py previously
existed here as one-line docstring stubs and were deleted — an empty module implies a
commitment the project has not made, and both extended manifests are still 100%
placeholder (sha256: "replace_me", base_uri: example.org). They return in v1.1,
gated on at least one real published archive with a real checksum. See
development-plan.md.
src/geocase/pytest_plugin/¶
Pytest integration.
fixtures.py¶
Provides fixtures like:
geocase_registrygeocase
markers.py¶
Custom pytest markers later, such as:
@pytest.mark.geocase_remote@pytest.mark.geocase_slow
Why it exists: GeoCase is designed for pytest, so first-class integration matters.
src/geocase/cli/ — removed¶
There is no CLI. The directory held five one-line docstring stubs while
pyproject.toml declared geocase = "geocase.cli.main:app", so every install got a
console script that died with ImportError. Both the entry point and the stubs were
removed for v1.0; a CLI is deferred to v1.1 and would be re-added only if maintainer
workflows need one. Catalog inspection and validation run through
scripts/validate_catalog.py in the meantime.
src/geocase/metadata/¶
Static metadata indexes and schemas.
metadata/case-index.yaml ✅¶
Master list of case metadata file paths (currently 8 cases).
Why it exists: gives one deterministic place to discover all cases.
metadata/suite-index.yaml ✅¶
Master list of suite definition file paths (currently 7 suites: core-vector, crs-edge-cases, raster-nodata,
vector-topology, vector-crs-edge, vector-schema-encoding,
georeferencing-conventions).
Why it exists: same idea, but for suites.
metadata/schemas/case.schema.yaml¶
Human-readable schema contract for case metadata.
metadata/schemas/suite.schema.yaml¶
Human-readable schema contract for suites.
metadata/schemas/manifest.schema.yaml¶
Contract for remote dataset manifests.
Why it exists: keeps structure explicit and reviewable.
src/geocase/data/core/ ✅¶
Bundled sample data. All 8 case.yaml files are populated with full metadata.
The catalog is 174 cases. The lists that were once here enumerated eight of them and went stale within one release; the enumeration is now generated from the registry and CI-gated instead. See the dataset catalog for the reasoning and the case catalog for the full list.
data/core/vector/ (103 cases)¶
A 66-case geometry × format baseline (6 geometry types across the formats that support
them), 36 special/ edge cases in eight families — crs, dateline, invalid,
encoding, precision, empty, degenerate, holes — and one GeometryCollection.
data/core/raster/ (30 cases)¶
GeoTIFFs in four groups: product families (17), the dtype family (5), the nodata/alignment/CRS family (3), and footprint edge cases (5).
Rotated and skewed affine transforms are not covered. affine_transform_quirk was an
empty stub claiming that coverage and was deleted in Batch 3; the gap is on the v1.1 list,
and validate_catalog.py now fails on any unindexed *.yaml under data/core.
data/core/netcdf/ (1 case)¶
latlon_small— CF-compliant lat/lon NetCDF
Each case folder contains:
case.yaml- main file like
.geojson,.gpkg,.tif,.nc - optional
notes.md - optional preview
Why it exists: every case should be self-contained.
src/geocase/templates/ ✅¶
Starter templates for contributors.
templates/new_case.yaml ✅¶
Fully commented template with all fields, inline guidance, and sensible defaults. Copy into a new case directory and fill in.
templates/notes.md¶
Template notes file.
Why it exists: reduces drift and makes contributions consistent.
tests/¶
Your own test suite for GeoCase itself.
tests/unit/¶
Fast tests for models, loaders, selectors, suites, assertions.
tests/integration/¶
Broader tests that exercise actual case loading across several cases.
tests/fixtures/¶
Shared test data for GeoCase’s own internal tests.
Why it exists: GeoCase should test its own testing toolkit.
examples/¶
Small realistic examples for users.
Why it exists: examples are often more useful than long prose docs.
scripts/¶
Maintenance scripts.
build_case_index.py¶
Rebuilds case-index.yaml.
validate_catalog.py¶
Validates all cases and indexes.
package_extended_cases.py¶
Packages remote/extended datasets.
generate_checksums.py¶
Creates SHA256 hashes.
Why it exists: separates contributor tooling from package runtime code.
extended-manifests/¶
Remote catalog metadata.
Why it exists: lets you describe large downloadable cases without shipping them inside the package.
3. What to code next, in order¶
This is the order I would follow in VS Code.
Step 1 — make catalog/models.py work ✅¶
This is your foundation.
Implemented:
CaseMetadatawith strictidvalidationSuiteMetadatawithSuiteSelectionusing Literal types- Helper models:
FileMap,SourceInfo,RemoteInfo,AssertionHints - Type aliases:
Category,FormatType,TestTier,SizeClass,StorageClass,LoaderHint,Status
Goal: a case.yaml should load cleanly into CaseMetadata. ✅
Step 2 — add catalog/loader.py ✅¶
This module does one job well:
- read YAML
- instantiate model
- return typed object
Implemented:
load_case_metadata(path) -> CaseMetadata
load_suite_metadata(path) -> SuiteMetadata
load_case_index(path) -> list[str]
load_suite_index(path) -> list[str]
Goal: make metadata loading deterministic and simple. ✅
Step 3 — implement catalog/registry.py ⬅️ NEXT¶
This reads case-index.yaml, loads all cases, and keeps them in memory.
Functions/methods you want:
get_registry()list_cases()get_case_record(case_id)- maybe
filter(...)
The registry should probably store records like:
- metadata
- case root directory
- resolved primary path
Goal: be able to say get_case("dateline_crossing_polygon").
Step 4 — implement catalog/selectors.py¶
This is very important because this is where your main value appears.
Functions to add:
select_cases(...)- maybe
select_case_ids(...)
Typical filters:
categorytest_tiertagrisk_typeformat_typestorage_class
Goal: make metadata-driven parameterization work.
Example target usage:
Step 5 — implement catalog/suites.py¶
Load suite YAML and resolve it to actual case objects.
Functions to add:
list_suites()load_suite_definition(name)suite(name)
Goal: let users write:
Step 6 — implement cases/base.py and cases/factory.py¶
BaseCase should hold:
- metadata
- root dir
primary_path
Factory should:
- inspect metadata category
- return
VectorCase,RasterCase, orNetCDFCase
Goal: convert catalog metadata into real usable objects.
Step 7 — implement category-specific case classes¶
You want at least:
VectorCase.load()RasterCase.open()NetCDFCase.load()
At first keep them minimal.
Goal: a case object should actually open its data.
Step 8 — add a few assertions¶
Start with a tiny set.
For vector:
- has CRS
- not empty
- geometry types
For raster:
- has CRS
- has nodata
Goal: make GeoCase more than a file catalog.
Step 9 — add tests for GeoCase itself¶
Do not postpone this too much.
Start with:
test_registry_loads_casestest_get_casetest_select_cases_by_categorytest_suite_resolves_cases
Then loader tests.
Goal: lock in the architecture before it grows.
Step 10 — add pytest_plugin/fixtures.py¶
Fixtures:
geocase_registrygeocase
Goal: give users a clean pytest experience.
4. A practical implementation sequence per file¶
This is the simplest coding order.
First wave ✅ (models + loader done, registry is next)¶
- ~~
catalog/models.py~~ ✅ - ~~
catalog/loader.py~~ ✅ catalog/registry.py⬅️ next
Second wave¶
cases/base.pycases/vector.pycases/raster.pycases/netcdf.pycases/factory.py
Third wave¶
catalog/selectors.pycatalog/suites.py
Fourth wave¶
assertions/geometry.pyassertions/crs.pyassertions/raster.py
Fifth wave¶
pytest_plugin/fixtures.py- tests
- examples
That order minimizes confusion.
5. What each major file should roughly contain¶
Here is the brief coding intent for the most important files.
catalog/models.py ✅¶
Contains only data models and type constraints. No file I/O. No registry logic. No GeoPandas/Rasterio code.
catalog/loader.py ✅¶
Contains YAML-reading functions. No selection logic. No pytest logic.
catalog/registry.py¶
Contains in-memory catalog assembly and lookup logic. No file-format loading.
catalog/selectors.py¶
Contains filtering logic over registry results.
catalog/suites.py¶
Contains named suite resolution logic.
cases/base.py¶
Contains common case behavior and file path helpers.
cases/vector.py, raster.py, netcdf.py¶
Contain data-format-specific loading.
cases/factory.py¶
Contains the mapping from metadata category to case class.
assertions/*¶
Contain reusable test helpers only.
pytest_plugin/fixtures.py¶
Contains pytest fixtures, not business logic.
That separation will save you a lot of refactoring later.
6. How to work with the VS Code LLM effectively¶
Since you want to continue in an integrated LLM, the best approach is small, specific prompts, not one giant request.
A good pattern is:
- ask it to implement one module
- ask it to add tests for that module
- review manually
- move to next module
Good prompt style:
Implement
src/geocase/catalog/models.pyfor GeoCase. Use Pydantic v2. Add models for case metadata and suite metadata based on the existing YAML schemas insrc/geocase/metadata/schemas/. Keep the file focused on models only. Include strict validation for case ID, enums for category/test tier/storage class, and default factories for list/dict fields.
Then:
Now implement
src/geocase/catalog/loader.py. Addload_case_metadata(path: Path) -> CaseMetadataandload_suite_metadata(path: Path) -> SuiteMetadata. Useyaml.safe_load. Keep the module small and typed.
Then:
Add unit tests for
catalog/models.pyandcatalog/loader.pyusing pytest. Include valid and invalid case metadata examples.
That workflow usually works much better than asking for the whole repo at once.
7. The minimum working milestone¶
Before doing remote manifests, CLI, or fancy assertions, aim for this milestone:
- one case can be loaded from YAML
- registry discovers all cases from
case-index.yaml get_case("simple_valid_polygon")worksselect_cases(category="vector", test_tier="unit")workssuite("core-vector")works- vector case
.load()works - one example pytest parameterization works
That is the first real “product moment”.
8. What not to overbuild yet¶
At this stage, I would avoid spending much time on:
- hosted catalog API
- database backend
- advanced CLI
- plugin system for private registries
- too many assertion types
- automatic remote download logic
- too many file formats
Keep the center strong first: metadata -> registry -> selection -> case loading -> pytest
9. A clean mental map of the architecture¶
You can think of the package like this:
Metadata layer¶
Defines what exists.
Files:
case.yamlsuite.yamlcase-index.yamlsuite-index.yaml
Python:
catalog/models.pycatalog/loader.py
Catalog layer¶
Finds and filters cases.
Python:
catalog/registry.pycatalog/selectors.pycatalog/suites.py
Runtime layer¶
Turns metadata into loadable objects.
Python:
cases/base.pycases/factory.pycases/vector.pycases/raster.pycases/netcdf.py
Testing helper layer¶
Improves test ergonomics.
Python:
assertions/*pytest_plugin/fixtures.py
Storage layer¶
Resolves where files live.
Python:
storage/*
This is the cleanest way to reason about the repo.
10. Your next practical move in VS Code¶
Wave 1 (metadata layer) is complete. Models and loader are implemented, all case.yaml files are populated, suite definitions exist, and config files are restored.
The next target is Wave 2 — the catalog layer:
Implement the catalog layer for GeoCase.
Scope:
1. src/geocase/catalog/registry.py
2. src/geocase/catalog/selectors.py
3. src/geocase/catalog/suites.py
Requirements:
- registry.py: read case-index.yaml, load all CaseMetadata, store in memory
- get_registry() -> Registry
- list_cases() -> list of case records
- get_case_record(case_id) -> single record or KeyError
- selectors.py: filter cases by category, test_tier, tags, format, storage_class, etc.
- select_cases(...) -> list of case records
- suites.py: load suite YAML, apply its SuiteSelection to the registry
- list_suites() -> list of suite keys
- suite(name) -> list of case records
- Add pytest unit tests for each module
- Keep concerns separated across the three files
After that, the prompt should target Wave 3 — the runtime layer:
Implement the runtime case layer for GeoCase.
Scope:
1. src/geocase/cases/base.py
2. src/geocase/cases/vector.py
3. src/geocase/cases/raster.py
4. src/geocase/cases/netcdf.py
5. src/geocase/cases/factory.py
Requirements:
- BaseCase should expose metadata, id, root_dir, and primary_path
- VectorCase.load() should use geopandas
- RasterCase.open() should use rasterio
- NetCDFCase.load() should use xarray
- factory.get_case(case_id) should resolve metadata from the registry and return the correct case subclass
- include helpful ImportError messages for missing optional dependencies
- add tests where practical
That will move you forward cleanly.
11. Final summary¶
The skeleton is built and the metadata layer is complete. Now the important part is to make the catalog layer real.
Completed:
- ~~
catalog/models.py~~ ✅ - ~~
catalog/loader.py~~ ✅ - ~~All
case.yamlfiles~~ ✅ - ~~Suite definitions~~ ✅
- ~~Config files (
pyproject.toml,.gitignore,LICENSE,mkdocs.yml)~~ ✅ - ~~
templates/new_case.yaml~~ ✅
Next coding priorities:
catalog/registry.pycatalog/selectors.pycatalog/suites.pycases/*- tests
- pytest fixtures
And the reason each folder exists is simple:
docs/explains the systemmetadata/defines the catalog contractcatalog/discovers and filters casescases/loads actual dataassertions/provides reusable test helperspytest_plugin/makes GeoCase natural in pyteststorage/handles local/remote artifactsdata/core/contains bundled, self-contained sample casestests/ensures GeoCase itself behaves correctly
The best next step is to implement the first working metadata/catalog slice before doing anything else.