Contributing¶
Thanks for contributing! This guide explains how we work, what a finished pull request looks like, and how to get set up. The short version: send a PR, test it against real data, get it reviewed hard, and let CI do its job.
How we work¶
Prefer pull requests to issues. If you can describe a problem clearly, you can usually also open a PR that fixes it. Open an issue instead only when the change genuinely needs discussion first (a design decision, a breaking change, a new dependency) or when you can't attempt the fix yourself.
Start from a reproducible example. A failing test that demonstrates a bug is a better bug report than any amount of prose. If you hit a bug, try to capture it as a small test case first. From there, an AI coding assistant (Claude, Copilot, or similar) can often carry you the rest of the way to a working PR — we encourage that. AI-assisted PRs are welcome here; they're held to the same bar as any other PR, and the CI is built so we can trust them.
The CI is strict on purpose. Every gate described below exists so that maintainers don't have to manually re-verify each change. Strict, automated checks are what make it safe for us to accept fast, autonomous contributions — including ones written largely by AI. The guardrails aren't there to slow you down; they're what let us say yes quickly.
What a finished PR looks like¶
Before you ask for a merge, check that your PR clears this bar:
- [ ] Tests run against real data. This project reads and writes real GeoParquet files, so tests should too. Use actual (small) files rather than mocks or toy fixtures wherever that's feasible.
- [ ] Integration tests for anything that crosses a boundary. If your change spans modules, file formats, or an external service, add a test that exercises the whole path — unit tests alone aren't enough.
- [ ] At least one adversarial review. Someone (or something) whose job is to break the change: feed it wrong data, hit the edge cases, probe the failure modes. An AI review counts, but it has to be adversarial in intent, not a rubber stamp.
- [ ] All CI checks green. The required checks are the contract. A red check blocks the merge, by design — fix it rather than working around it.
- [ ] CodeRabbit comments addressed. Every automated review comment is either fixed or answered with a reason. Silence isn't an answer.
- [ ] Docs updated if you changed user-facing behavior (guides, CLI reference, Python API docs).
Prerequisites¶
- Python 3.10+
- uv
Setup¶
git clone https://github.com/geoparquet/geoparquet-io.git
cd geoparquet-io
uv sync --all-extras
uv run pre-commit install
Tests¶
uv run pytest # Full suite
uv run pytest tests/test_yourfile.py -v # Single file
uv run pytest -m "not slow and not network" # Fast tests only
CI runs three test tiers:
- Fast tests (
not slow and not network) — run on every PR across the full OS/Python matrix and block merging. - Slow tests (
slow and not network) — run after merge to main and nightly; opt in on a PR by adding therun-slow-testslabel. - Network tests (
network) — hit live third-party services (ArcGIS, WFS, Carto, ...). They are non-blocking: failures open/update a tracking issue instead of failing CI, because a remote server's behavior is not something a PR author can fix. They run serially with per-test timeouts, retries, and process isolation (--forked).
Coverage gates (both enforced in CI):
- Global floor: 67% total coverage.
- Changed lines: 90% —
diff-coverchecks that the lines your PR touches are covered by fast tests. New code ships with tests that run on every PR, not only in the post-merge slow suite.
Test Markers¶
| Marker | Description |
|---|---|
@pytest.mark.slow |
marks tests as slow (deselect with '-m "not slow"') |
@pytest.mark.network |
marks tests requiring network access (deselect with '-m "not network"') |
@pytest.mark.integration |
marks end-to-end integration tests |
Code Quality¶
.pre-commit-config.yaml is the single source of truth for every quality
rule. CI runs the exact same hooks (pre-commit run --all-files, plus the
pre-push stage), so a check can never pass locally and fail in CI for a
different rule set — and skipping local hook install just moves the same
failure to CI.
uv run pre-commit run --all-files # commit-stage hooks
uv run pre-commit run --all-files --hook-stage pre-push # deptry, vulture,
# xenon, mypy,
# import-linter, menard
Two ratchet/ignore files gate quality over time:
.pip-audit-ignores— self-expiring CVE ignore list shared by the CI security job and the daily security audit. Each entry needs an expiry date and a reason; expired entries automatically re-fail CI..mutation-baseline— minimum mutation kill rate enforced by the nightly mutation-testing run. Ratchet it up as test quality improves.
How the CI is set up (and why)¶
Merging to main requires these status checks to pass: lint, security,
notebooks, and the test matrix (Ubuntu on Python 3.10–3.13, macOS and
Windows on 3.11–3.13). That list lives in code —
scripts/apply_branch_protection.sh declares the required checks and review
rules as GitHub rulesets, and an admin re-runs it whenever the list changes.
If your PR is blocked on a check, that's the system working as intended:
fix the check.
Two heavier gates exist but don't run on every PR, so they won't slow down day-to-day contributions:
- Benchmark regression — label-triggered; fails if performance drops 10% or more against the baseline.
- Mutation testing — nightly; enforces the kill-rate floor in
.mutation-baseline.
The repository also maintains itself where it can:
- The daily security audit opens an automated fix PR when a dependency vulnerability has a clean upgrade path.
- Dependabot PRs auto-merge once all required checks pass.
Bot-opened PRs go through the exact same required checks as human ones. That's the deal across the board: nothing merges without the checks, and because of that, a lot can merge without a human bottleneck.
Documentation¶
uv run mkdocs serve
Commits¶
Use Conventional Commits: type(scope): message
Types: feat, fix, docs, refactor, test, chore
Architecture¶
New CLI commands need corresponding Python API:
- Core logic in
geoparquet_io/core/<feature>.py - CLI wrapper in
geoparquet_io/cli/main.py - Python API in
geoparquet_io/api/table.pyandapi/ops.py
See CLAUDE.md for full architecture details.
External extractors¶
Extractors that page through a remote service (wfs, arcgis, carto, bigquery) keep
re-discovering the same edge cases. Reuse the shared schema helpers in core/common.py
(_compute_unified_schema, _cast_table_to_schema, _promote_numeric_type) rather than
hand-rolling schema reconciliation — the forbid-bespoke-schema-reconciliation pre-commit hook
enforces this. Known edge cases to handle:
- Pagination: detect server
startIndex/page-size limits; supply a stable sort (sortBy/orderByFields) for layers without a primary key. - Schema across pages: never trust the first page's inferred schema. Where a source has an
authoritative schema (ArcGIS layer metadata), cast each page to it via
_cast_table_to_schema. Where it does not (WFS), unify with_compute_unified_schema(handles int/float/decimal mixes, uint64 overflow). - Empty/null responses: handle empty result sets and null properties without crashing.
- CRS/SR: normalize WKID/SR; resolve native WKT → EPSG; unset CRS for unresolvable codes; honor
--output-crsby reprojecting when the server returns a different CRS. - Network: retry transient errors; surface upstream stderr.
Releasing¶
(Maintainers only)
Uses commitizen + automated CI workflow:
# 1. Create bump PR (updates version + changelog)
uv run cz bump --changelog
git push origin HEAD
# 2. Open PR, merge to main
# 3. CI automatically: creates tag, publishes to PyPI, creates GitHub Release
Recovery (if release fails after merge):
git push origin :refs/tags/vX.Y.Z # Delete orphan tag
gh workflow run release.yml --ref main # Retry
License¶
By contributing, you agree that your contributions will be licensed under the Apache 2.0 License.