Data contracts for geospatial pipelines
Project description
geoassert
Validate GeoParquet files before bad data reaches production.
geoassert is a Python library and CLI that runs contract-based validation on geospatial datasets — GeoParquet files, PostGIS tables, BigQuery GIS tables, Snowflake geometry columns, and dbt model outputs. It catches CRS mismatches, broken metadata, invalid geometries, out-of-bounds data, and attribute problems before they silently corrupt downstream maps, models, and analytics.
pip install geoassert
geoassert validate data/buildings.parquet --contract contracts/buildings.yml
Why geoassert?
Geospatial data fails in ways general-purpose validators don't understand:
- A GeoParquet file with an empty or stale bounding box in its metadata
- A CRS that looks right but has swapped lat/lon axes
- A PostGIS export where 3% of polygons are topologically invalid
- A partitioned dataset where one month quietly changed schema
geoassert sits above GeoPandas, Shapely, and PyArrow as a lightweight validation layer — fast enough to run in every CI job, specific enough to catch the problems that matter.
Install
pip install geoassert
Optional extras for richer checks:
pip install "geoassert[shapely]" # geometry validity and type checks
pip install "geoassert[duckdb]" # scalable attribute checks via DuckDB SQL
pip install "geoassert[postgis]" # validate PostGIS tables directly
pip install "geoassert[bigquery]" # validate BigQuery GIS tables
pip install "geoassert[snowflake]" # validate Snowflake geometry columns
pip install "geoassert[all]" # everything
Reproducible quickstart
A 120-row sample GeoParquet file and matching contract are bundled under examples/ — every command below runs against them out of the box.
git clone https://github.com/Morphenr/geoassert
cd geoassert
pip install "geoassert[shapely]"
# 1. Profile the sample dataset
geoassert profile examples/buildings.parquet
# 2. Validate against the bundled contract
geoassert validate examples/buildings.parquet --contract examples/buildings_contract.yml
Expected output:
──────────────── buildings.parquet ────────────────
PASS geoparquet.geo_metadata
PASS geoparquet.primary_column
PASS geoparquet.column_in_schema
PASS geoparquet.encoding
PASS geoparquet.crs_parseable
PASS geoparquet.geometry_types
PASS geoparquet.row_group_stats
PASS crs.exists
PASS crs.match
PASS bounds.available
PASS bounds.within
WARN bounds.bbox_consistency
PASS geometry.column_exists
PASS geometry.valid
PASS geometry.empty
PASS geometry.type
PASS attributes.building_id.exists
PASS attributes.building_id.nullable
PASS attributes.building_id.unique
PASS attributes.height_m.exists
PASS attributes.height_m.nullable
PASS attributes.height_m.range
All checks passed.
See examples/buildings.parquet and examples/buildings_contract.yml.
Full walkthrough: Quickstart →
Typical workflow
# 1. Profile a dataset — no contract needed
geoassert profile examples/buildings.parquet
# 2. Generate a starter contract from the data
geoassert init-contract examples/buildings.parquet > contracts/my_buildings.yml
# 3. Edit the contract to tighten constraints, then validate
geoassert validate examples/buildings.parquet --contract examples/buildings_contract.yml
# 4. Validate a whole directory of partitioned files
geoassert validate data/partitioned/ --contract contracts/buildings.yml
Contract format
dataset: buildings
geometry:
column: geometry
type:
- Polygon
- MultiPolygon
crs: EPSG:4326
valid: true
allow_empty: false
bounds:
within:
bbox: [-8.65, 49.86, 1.76, 60.86] # Great Britain
attributes:
building_id:
nullable: false
unique: true
height_m:
nullable: true
min: 0
max: 400
What it checks
| Category | Checks |
|---|---|
| GeoParquet | geo metadata present, primary column, schema, encoding, CRS parseable, geometry types, row group statistics |
| CRS | CRS present, CRS matches contract |
| Bounds | bbox metadata present, dataset within expected bbox, declared bbox matches actual geometry bounds |
| Geometry | column exists, validity, empty geometries, type distribution |
| Attributes | column exists, nullable, unique, min/max range |
| Partitions | Hive structure detected, schema consistency across partition files |
Warehouse support
Validate GeoParquet files, PostGIS tables, BigQuery GIS tables, and Snowflake geometry columns against the same contract format:
# PostGIS
geoassert validate postgis://user:pass@host/db/public/buildings \
--contract contracts/buildings.yml
# BigQuery
geoassert validate bigquery://my-project/my_dataset/buildings \
--contract contracts/buildings.yml
# Snowflake
geoassert validate snowflake://myaccount/MY_DB/PUBLIC/BUILDINGS \
--contract contracts/buildings.yml \
--sf-user myuser --sf-password secret
Or via Python:
from geoassert.engines.postgis import read_postgis_info
import geoassert as ga
info = read_postgis_info("postgresql://user:pass@host/db", "buildings")
result = ga.validate_source(info, "contracts/buildings.yml")
dbt integration
# List all models in the dbt manifest
geoassert dbt list
# Validate a specific model
geoassert dbt validate buildings \
--contract contracts/buildings.yml \
--dsn postgresql://user:pass@host/db
CI integration
- name: Validate GeoParquet files
run: |
pip install geoassert
geoassert validate data/buildings.parquet \
--contract contracts/buildings.yml \
--format github
--format github emits ::error and ::warning annotations directly in the GitHub Actions UI.
Python API
import geoassert as ga
result = ga.validate(
"data/buildings.parquet",
contract="contracts/buildings.yml",
)
if not result.passed:
print(result.to_markdown())
assert result.passed
Failure messages
Every failed check explains what failed, what was expected, what was observed, and how to fix it:
{
"check": "crs.match",
"status": "fail",
"severity": "error",
"expected": "EPSG:4326",
"observed": "OGC:CRS84",
"message": "CRS metadata differs from contract.",
"suggestion": "Normalise CRS metadata before export or set allow_equivalent_crs: true if intentional."
}
Exit codes
| Code | Meaning |
|---|---|
| 0 | all checks passed |
| 1 | validation failures |
| 2 | invalid contract / configuration |
| 3 | unreadable input data |
| 4 | internal error |
Roadmap
| Version | Status | Highlights |
|---|---|---|
| v0.1 | ✅ shipped | Core checks: GeoParquet, CRS, bounds, geometry, attributes |
| v0.2 | ✅ shipped | GitHub Actions annotations, JUnit XML, sampling, configurable severity |
| v0.3 | ✅ shipped | DuckDB backend, cloud paths (S3/GCS/Azure), partition validation, row group checks |
| v0.4 | ✅ shipped | PostGIS, BigQuery GIS, Snowflake engines; dbt integration; validate_source() API |
| v0.5 | planned | Spatial drift checks, H3 coverage validation, HTML reports |
Non-goals
geoassert is not a GIS analysis library, a GeoPandas replacement, or a spatial database. It sits above them as a validation layer.
Contributing
Bug reports and check requests welcome — use the GitHub issue templates.
git clone https://github.com/Morphenr/geoassert
cd geoassert
uv sync --extra dev
uv run pytest
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file geoassert-0.6.1.tar.gz.
File metadata
- Download URL: geoassert-0.6.1.tar.gz
- Upload date:
- Size: 432.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8442641a0ee47ced895ae28cd266ae3265da18b3fb7eeabdbadcdf6e8318cfad
|
|
| MD5 |
41530118b307edf9c294efcc7af429a5
|
|
| BLAKE2b-256 |
43a1d304d002b5dfd574e7e1e21471ef04d0ec928e91ba58f0279fc4ff2de33c
|
Provenance
The following attestation bundles were made for geoassert-0.6.1.tar.gz:
Publisher:
publish.yml on Morphenr/geoassert
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
geoassert-0.6.1.tar.gz -
Subject digest:
8442641a0ee47ced895ae28cd266ae3265da18b3fb7eeabdbadcdf6e8318cfad - Sigstore transparency entry: 1574370635
- Sigstore integration time:
-
Permalink:
Morphenr/geoassert@58092c553bc2caf839f752cf7abd6a84469f165b -
Branch / Tag:
refs/tags/v0.6.1 - Owner: https://github.com/Morphenr
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@58092c553bc2caf839f752cf7abd6a84469f165b -
Trigger Event:
release
-
Statement type:
File details
Details for the file geoassert-0.6.1-py3-none-any.whl.
File metadata
- Download URL: geoassert-0.6.1-py3-none-any.whl
- Upload date:
- Size: 51.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
acc24499c26781cd37350ecc3489212dd029072cb31bd7068c772a5fb56c5f88
|
|
| MD5 |
6c431fda5718b3a407cb304e801303c6
|
|
| BLAKE2b-256 |
67a5829ceb8c749866c4bc5069cdb197013a69fc31ea199e71837703c58c3731
|
Provenance
The following attestation bundles were made for geoassert-0.6.1-py3-none-any.whl:
Publisher:
publish.yml on Morphenr/geoassert
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
geoassert-0.6.1-py3-none-any.whl -
Subject digest:
acc24499c26781cd37350ecc3489212dd029072cb31bd7068c772a5fb56c5f88 - Sigstore transparency entry: 1574370659
- Sigstore integration time:
-
Permalink:
Morphenr/geoassert@58092c553bc2caf839f752cf7abd6a84469f165b -
Branch / Tag:
refs/tags/v0.6.1 - Owner: https://github.com/Morphenr
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@58092c553bc2caf839f752cf7abd6a84469f165b -
Trigger Event:
release
-
Statement type: