Skip to content

Commit 951421f

Browse files
committed
implement geospec: RFC 7946 GeoJSON library
0 parents  commit 951421f

26 files changed

Lines changed: 1761 additions & 0 deletions

‎.github/dependabot.yml‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
version: 2
2+
updates:
3+
- package-ecosystem: "pip"
4+
directory: "/"
5+
schedule:
6+
interval: "weekly"
7+
- package-ecosystem: "github-actions"
8+
directory: "/"
9+
schedule:
10+
interval: "weekly"

‎.github/workflows/ci.yml‎

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
jobs:
10+
test:
11+
runs-on: ${{ matrix.os }}
12+
strategy:
13+
matrix:
14+
os: [ubuntu-latest, macos-latest]
15+
python-version: ["3.10", "3.11", "3.12", "3.13"]
16+
fail-fast: false
17+
steps:
18+
- uses: actions/checkout@v6
19+
- uses: actions/setup-python@v5
20+
with:
21+
python-version: ${{ matrix.python-version }}
22+
- name: Install uv
23+
uses: astral-sh/setup-uv@v5
24+
- name: Install dependencies
25+
run: uv sync
26+
- name: Run tests
27+
run: uv run pytest tests/ -v
28+
29+
typecheck:
30+
runs-on: ubuntu-latest
31+
steps:
32+
- uses: actions/checkout@v6
33+
- uses: actions/setup-python@v5
34+
with:
35+
python-version: "3.13"
36+
- name: Install uv
37+
uses: astral-sh/setup-uv@v5
38+
- name: Install dependencies
39+
run: uv sync
40+
- name: Run mypy
41+
run: uv run mypy src/ --strict

‎.github/workflows/publish.yml‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
name: Publish to PyPI
2+
3+
on:
4+
release:
5+
types: [published]
6+
7+
jobs:
8+
publish:
9+
runs-on: ubuntu-latest
10+
environment: pypi
11+
permissions:
12+
id-token: write
13+
steps:
14+
- uses: actions/checkout@v6
15+
- name: Install uv
16+
uses: astral-sh/setup-uv@v5
17+
- name: Build distribution
18+
run: uv build
19+
- name: Publish to PyPI
20+
run: uv publish

‎.gitignore‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
__pycache__/
2+
*.py[cod]
3+
*$py.class
4+
.venv/
5+
.env
6+
dist/
7+
build/
8+
*.egg-info/
9+
.mypy_cache/
10+
.pytest_cache/
11+
.ruff_cache/
12+
coverage/
13+
htmlcov/
14+
.coverage
15+
uv.lock

‎CHANGELOG.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Changelog
2+
3+
## 0.1.0
4+
5+
- Initial release

‎LICENSE‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
BSD 3-Clause License
2+
3+
Copyright (c) 2026, Agentine
4+
5+
Redistribution and use in source and binary forms, with or without
6+
modification, are permitted provided that the following conditions are met:
7+
8+
1. Redistributions of source code must retain the above copyright notice, this
9+
list of conditions and the following disclaimer.
10+
11+
2. Redistributions in binary form must reproduce the above copyright notice,
12+
this list of conditions and the following disclaimer in the documentation
13+
and/or other materials provided with the distribution.
14+
15+
3. Neither the name of the copyright holder nor the names of its
16+
contributors may be used to endorse or promote products derived from
17+
this software without specific prior written permission.
18+
19+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

‎Makefile‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
.PHONY: install build test lint fmt clean ci
2+
3+
install:
4+
uv sync
5+
6+
build:
7+
uv build
8+
9+
test:
10+
uv run pytest tests/ -v
11+
12+
lint:
13+
uv run ruff check src/
14+
uv run mypy src/
15+
16+
fmt:
17+
uv run ruff format src/ tests/
18+
19+
clean:
20+
rm -rf dist/ build/ *.egg-info/ .pytest_cache/ .mypy_cache/
21+
22+
ci: lint test

‎PLAN.md‎

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
# geospec — GeoJSON for Python (RFC 7946)
2+
3+
**Replaces:** [jazzband/geojson](https://github.com/jazzband/geojson) (4.5M/month PyPI, 984 stars)
4+
**Package name:** `geospec` (verified available on PyPI)
5+
**Language:** Python (3.10+)
6+
**License:** BSD-3-Clause
7+
8+
## Why
9+
10+
The jazzband/geojson package is at risk:
11+
- Jazzband is sunsetting by end of 2026 (AI spam crisis, governance failure)
12+
- Last meaningful commit: Dec 2024 (release 3.2.0)
13+
- Python 3.14 compatibility PR sitting unmerged for 3 months
14+
- 28 open issues, 25 open PRs — no maintainer response
15+
- No standalone drop-in alternative exists (geojson-pydantic requires Pydantic, different API)
16+
17+
## Scope
18+
19+
Lightweight, zero-dependency Python library implementing RFC 7946 GeoJSON objects with:
20+
- All 7 geometry types + Feature + FeatureCollection
21+
- JSON serialization/deserialization with GeoJSON object hooks
22+
- Coordinate validation per RFC 7946
23+
- `__geo_interface__` protocol support
24+
- Coordinate utility functions (extraction, transformation)
25+
26+
## Architecture
27+
28+
```
29+
geospec/
30+
├── __init__.py # Public API re-exports
31+
├── _types.py # Type aliases (Coord2D, Coord3D, BBox, etc.)
32+
├── base.py # GeoJSON base class (dict subclass)
33+
├── geometry.py # Point, LineString, Polygon, Multi*, GeometryCollection
34+
├── feature.py # Feature, FeatureCollection
35+
├── codec.py # dump, dumps, load, loads, GeoJSONEncoder
36+
├── validation.py # RFC 7946 validation logic (separated from geometry)
37+
├── utils.py # coords, map_coords, map_tuples, map_geometries
38+
└── py.typed # PEP 561 marker
39+
```
40+
41+
## Key Design Decisions
42+
43+
1. **Dict subclass** — same as jazzband/geojson. GeoJSON objects behave as dicts with attribute access. This ensures JSON serialization compatibility and familiar API.
44+
45+
2. **Separated validation** — validation logic in its own module for testability. Each geometry type delegates to validation functions.
46+
47+
3. **Type hints throughout** — full type annotations with `py.typed` marker. TypeAlias for coordinate types (`Coord2D = tuple[float, float]`, `Coord3D = tuple[float, float, float]`).
48+
49+
4. **RFC 7946 strictness** — optional strict mode validates:
50+
- Longitude range [-180, 180], Latitude range [-90, 90]
51+
- Right-hand rule for polygon winding order
52+
- Linear ring closure (first == last coordinate)
53+
- Minimum coordinate counts per geometry type
54+
55+
5. **Zero dependencies** — stdlib `json` only. No simplejson fallback (unnecessary for modern Python).
56+
57+
6. **API compatibility** — drop-in replacement for jazzband/geojson:
58+
- Same class names: `Point`, `LineString`, `Polygon`, `MultiPoint`, `MultiLineString`, `MultiPolygon`, `GeometryCollection`, `Feature`, `FeatureCollection`
59+
- Same functions: `dump`, `dumps`, `load`, `loads`, `coords`, `map_coords`
60+
- Same `__geo_interface__` protocol
61+
- Same `errors()` / `is_valid` API
62+
- Same dict-like behavior with attribute access
63+
64+
## Public API
65+
66+
### Classes
67+
- `GeoJSON` — base class
68+
- `Point(coordinates, validate=False, precision=6)`
69+
- `LineString(coordinates, validate=False, precision=6)`
70+
- `Polygon(coordinates, validate=False, precision=6)`
71+
- `MultiPoint(coordinates, validate=False, precision=6)`
72+
- `MultiLineString(coordinates, validate=False, precision=6)`
73+
- `MultiPolygon(coordinates, validate=False, precision=6)`
74+
- `GeometryCollection(geometries=None)`
75+
- `Feature(id=None, geometry=None, properties=None)`
76+
- `FeatureCollection(features)`
77+
78+
### Functions
79+
- `dump(obj, fp)` / `dumps(obj)` — serialize to GeoJSON
80+
- `load(fp)` / `loads(s)` — deserialize from GeoJSON
81+
- `coords(obj)` — yield coordinate tuples from any GeoJSON object
82+
- `map_coords(func, obj)` — apply function to each coordinate dimension
83+
- `map_tuples(func, obj)` — apply function to each coordinate tuple
84+
- `map_geometries(func, obj)` — apply function to each geometry
85+
86+
### Properties
87+
- `obj.is_valid` — bool, True if no validation errors
88+
- `obj.errors()` — list of validation error strings
89+
- `obj.__geo_interface__` — GeoJSON-compatible dict
90+
91+
## Improvements Over jazzband/geojson
92+
93+
1. **Type hints** — full annotations, `py.typed` marker for IDE support
94+
2. **RFC 7946 range validation** — optional lat/lon bounds checking
95+
3. **Winding order validation** — check right-hand rule for polygons
96+
4. **No simplejson dependency** — stdlib json only
97+
5. **Python 3.10+** — modern syntax, no legacy compatibility code
98+
6. **Separated validation module** — cleaner, more testable
99+
100+
## Deliverables
101+
102+
- [ ] Core implementation (all classes, codec, validation, utils)
103+
- [ ] Comprehensive test suite (pytest)
104+
- [ ] pyproject.toml with modern packaging
105+
- [ ] CI/CD (GitHub Actions)
106+
- [ ] Migration guide from jazzband/geojson

‎README.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# geospec
2+
3+
Drop-in Python replacement for jazzband/geojson (RFC 7946, zero deps, Python 3.10+)

‎pyproject.toml‎

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
[project]
2+
name = "geospec"
3+
version = "0.1.0"
4+
description = "Drop-in Python replacement for jazzband/geojson (RFC 7946, zero deps, Python 3.10+)"
5+
readme = "README.md"
6+
license = "BSD-3-Clause"
7+
authors = [
8+
{ name = "Agentine" }
9+
]
10+
requires-python = ">=3.10"
11+
dependencies = []
12+
classifiers = [
13+
"Development Status :: 3 - Alpha",
14+
"Intended Audience :: Developers",
15+
"License :: OSI Approved :: BSD License",
16+
"Programming Language :: Python :: 3",
17+
"Programming Language :: Python :: 3.10",
18+
"Programming Language :: Python :: 3.11",
19+
"Programming Language :: Python :: 3.12",
20+
"Programming Language :: Python :: 3.13",
21+
"Typing :: Typed",
22+
]
23+
24+
[project.urls]
25+
Homepage = "https://github.com/agentine/geospec"
26+
Repository = "https://github.com/agentine/geospec"
27+
28+
[build-system]
29+
requires = ["hatchling"]
30+
build-backend = "hatchling.build"
31+
32+
[tool.hatch.build.targets.wheel]
33+
packages = ["src/geospec"]
34+
35+
[dependency-groups]
36+
dev = [
37+
"mypy>=1.19",
38+
"pytest>=9.0",
39+
"ruff>=0.11",
40+
]
41+
42+
[tool.pytest.ini_options]
43+
testpaths = ["tests"]
44+
45+
[tool.mypy]
46+
strict = true
47+
warn_return_any = true
48+
warn_unused_configs = true

0 commit comments

Comments
 (0)