spatial-data-foundation 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. spatial_data_foundation-0.1.0/PKG-INFO +29 -0
  2. spatial_data_foundation-0.1.0/README.md +56 -0
  3. spatial_data_foundation-0.1.0/pyproject.toml +41 -0
  4. spatial_data_foundation-0.1.0/setup.cfg +4 -0
  5. spatial_data_foundation-0.1.0/src/spatial_data_foundation.egg-info/PKG-INFO +29 -0
  6. spatial_data_foundation-0.1.0/src/spatial_data_foundation.egg-info/SOURCES.txt +29 -0
  7. spatial_data_foundation-0.1.0/src/spatial_data_foundation.egg-info/dependency_links.txt +1 -0
  8. spatial_data_foundation-0.1.0/src/spatial_data_foundation.egg-info/requires.txt +29 -0
  9. spatial_data_foundation-0.1.0/src/spatial_data_foundation.egg-info/top_level.txt +1 -0
  10. spatial_data_foundation-0.1.0/src/spatial_foundation/__init__.py +20 -0
  11. spatial_data_foundation-0.1.0/src/spatial_foundation/catalog.py +52 -0
  12. spatial_data_foundation-0.1.0/src/spatial_foundation/geography/__init__.py +19 -0
  13. spatial_data_foundation-0.1.0/src/spatial_foundation/geography/gadm.py +92 -0
  14. spatial_data_foundation-0.1.0/src/spatial_foundation/geography/materialize.py +450 -0
  15. spatial_data_foundation-0.1.0/src/spatial_foundation/geography/membership.py +74 -0
  16. spatial_data_foundation-0.1.0/src/spatial_foundation/geography/models.py +37 -0
  17. spatial_data_foundation-0.1.0/src/spatial_foundation/geography/overlap.py +178 -0
  18. spatial_data_foundation-0.1.0/src/spatial_foundation/periods.py +48 -0
  19. spatial_data_foundation-0.1.0/src/spatial_foundation/presentation/__init__.py +10 -0
  20. spatial_data_foundation-0.1.0/src/spatial_foundation/presentation/basemaps.py +85 -0
  21. spatial_data_foundation-0.1.0/src/spatial_foundation/presentation/plotting.py +32 -0
  22. spatial_data_foundation-0.1.0/tests/test_adversarial_spatial.py +165 -0
  23. spatial_data_foundation-0.1.0/tests/test_areal_overlap.py +262 -0
  24. spatial_data_foundation-0.1.0/tests/test_catalog.py +29 -0
  25. spatial_data_foundation-0.1.0/tests/test_distribution.py +112 -0
  26. spatial_data_foundation-0.1.0/tests/test_external_consumer.py +28 -0
  27. spatial_data_foundation-0.1.0/tests/test_gadm_materialization.py +138 -0
  28. spatial_data_foundation-0.1.0/tests/test_geography.py +63 -0
  29. spatial_data_foundation-0.1.0/tests/test_membership_kernel.py +123 -0
  30. spatial_data_foundation-0.1.0/tests/test_periods.py +26 -0
  31. spatial_data_foundation-0.1.0/tests/test_presentation.py +144 -0
@@ -0,0 +1,29 @@
1
+ Metadata-Version: 2.4
2
+ Name: spatial-data-foundation
3
+ Version: 0.1.0
4
+ Summary: Reusable geography, time, provenance and spatial-membership infrastructure for empirical research
5
+ Requires-Python: >=3.10
6
+ Requires-Dist: empirical-data-contracts<0.2,>=0.1
7
+ Requires-Dist: geopandas>=0.14
8
+ Requires-Dist: shapely>=2
9
+ Requires-Dist: pyproj>=3.5
10
+ Requires-Dist: pydantic<3,>=2.7
11
+ Provides-Extra: io
12
+ Requires-Dist: pyarrow>=15; extra == "io"
13
+ Requires-Dist: duckdb>=1.0; extra == "io"
14
+ Provides-Extra: raster
15
+ Requires-Dist: rasterio>=1.3; extra == "raster"
16
+ Provides-Extra: cli
17
+ Requires-Dist: typer>=0.12; extra == "cli"
18
+ Provides-Extra: presentation
19
+ Requires-Dist: matplotlib>=3.8; extra == "presentation"
20
+ Requires-Dist: contextily<2,>=1.7.1; extra == "presentation"
21
+ Requires-Dist: xyzservices<2027,>=2026.3; extra == "presentation"
22
+ Provides-Extra: dev
23
+ Requires-Dist: pytest>=8; extra == "dev"
24
+ Requires-Dist: ruff>=0.6; extra == "dev"
25
+ Requires-Dist: pyarrow>=15; extra == "dev"
26
+ Requires-Dist: build>=1.2; extra == "dev"
27
+ Requires-Dist: matplotlib>=3.8; extra == "dev"
28
+ Requires-Dist: contextily<2,>=1.7.1; extra == "dev"
29
+ Requires-Dist: xyzservices<2027,>=2026.3; extra == "dev"
@@ -0,0 +1,56 @@
1
+ # spatial-data-foundation
2
+
3
+ Reusable spatial/time infrastructure for research systems.
4
+
5
+ This repository owns geography authority, period indexing, source registration, auditable spatial membership, and materialization provenance. It does not own FCV treatments, outcomes, matching, survey harmonization, or regressions.
6
+
7
+ Initial provider: GADM. Initial clients in the next build pack: GHSL and ACLED.
8
+
9
+ ## Operational GADM materialization
10
+
11
+ After registering local GADM files as an immutable source snapshot, install the IO extra and materialize the native levels needed by downstream work:
12
+
13
+ ```python
14
+ from spatial_foundation import materialize_gadm
15
+
16
+ materialize_gadm(
17
+ snapshot=gadm_4_1_snapshot,
18
+ levels=[0, 1, 2, 3],
19
+ output_root=data_root,
20
+ )
21
+ ```
22
+
23
+ This publishes full-geometry GeoParquet under `silver/geography/gadm/<version>/` and writes `run_manifest.json` plus `geography_qa.json` under the run directory. Registered source hashes are rechecked before publication; source drift produces a RED run instead of silently accepting changed bytes.
24
+
25
+ ## Optional contextual presentation
26
+
27
+ Presentation helpers are an optional, non-analytical surface. Core imports do not require plotting or tile dependencies:
28
+
29
+ ```bash
30
+ pip install "spatial-data-foundation[presentation]"
31
+ ```
32
+
33
+ Add contextual imagery beneath an existing analytical plot:
34
+
35
+ ```python
36
+ from spatial_foundation.presentation import add_basemap
37
+
38
+ ax = gdf.plot(column="value", alpha=0.65)
39
+ add_basemap(ax, crs=gdf.crs, kind="imagery", alpha=0.55)
40
+ ```
41
+
42
+ Or use the compact GeoDataFrame helper:
43
+
44
+ ```python
45
+ from spatial_foundation.presentation import plot_context
46
+
47
+ ax = plot_context(
48
+ gdf,
49
+ basemap="neutral",
50
+ column="value",
51
+ )
52
+ ```
53
+
54
+ The governed convenience aliases are `neutral`, `imagery`, and `terrain`. A concrete tile source or local raster path can also be supplied. Local rasters allow reproducible/offline presentation; web providers may perform network requests when explicitly selected by the caller.
55
+
56
+ Presentation never changes analytical geometry, membership, identifiers, or domain interpretation. Provider attribution remains enabled, and no provider credentials are bundled with this package.
@@ -0,0 +1,41 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "spatial-data-foundation"
7
+ version = "0.1.0"
8
+ description = "Reusable geography, time, provenance and spatial-membership infrastructure for empirical research"
9
+ requires-python = ">=3.10"
10
+ dependencies = [
11
+ "empirical-data-contracts>=0.1,<0.2",
12
+ "geopandas>=0.14",
13
+ "shapely>=2",
14
+ "pyproj>=3.5",
15
+ "pydantic>=2.7,<3",
16
+ ]
17
+
18
+ [project.optional-dependencies]
19
+ io = ["pyarrow>=15", "duckdb>=1.0"]
20
+ raster = ["rasterio>=1.3"]
21
+ cli = ["typer>=0.12"]
22
+ presentation = [
23
+ "matplotlib>=3.8",
24
+ "contextily>=1.7.1,<2",
25
+ "xyzservices>=2026.3,<2027",
26
+ ]
27
+ dev = [
28
+ "pytest>=8",
29
+ "ruff>=0.6",
30
+ "pyarrow>=15",
31
+ "build>=1.2",
32
+ "matplotlib>=3.8",
33
+ "contextily>=1.7.1,<2",
34
+ "xyzservices>=2026.3,<2027",
35
+ ]
36
+
37
+ [tool.setuptools.packages.find]
38
+ where = ["src"]
39
+
40
+ [tool.pytest.ini_options]
41
+ testpaths = ["tests"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,29 @@
1
+ Metadata-Version: 2.4
2
+ Name: spatial-data-foundation
3
+ Version: 0.1.0
4
+ Summary: Reusable geography, time, provenance and spatial-membership infrastructure for empirical research
5
+ Requires-Python: >=3.10
6
+ Requires-Dist: empirical-data-contracts<0.2,>=0.1
7
+ Requires-Dist: geopandas>=0.14
8
+ Requires-Dist: shapely>=2
9
+ Requires-Dist: pyproj>=3.5
10
+ Requires-Dist: pydantic<3,>=2.7
11
+ Provides-Extra: io
12
+ Requires-Dist: pyarrow>=15; extra == "io"
13
+ Requires-Dist: duckdb>=1.0; extra == "io"
14
+ Provides-Extra: raster
15
+ Requires-Dist: rasterio>=1.3; extra == "raster"
16
+ Provides-Extra: cli
17
+ Requires-Dist: typer>=0.12; extra == "cli"
18
+ Provides-Extra: presentation
19
+ Requires-Dist: matplotlib>=3.8; extra == "presentation"
20
+ Requires-Dist: contextily<2,>=1.7.1; extra == "presentation"
21
+ Requires-Dist: xyzservices<2027,>=2026.3; extra == "presentation"
22
+ Provides-Extra: dev
23
+ Requires-Dist: pytest>=8; extra == "dev"
24
+ Requires-Dist: ruff>=0.6; extra == "dev"
25
+ Requires-Dist: pyarrow>=15; extra == "dev"
26
+ Requires-Dist: build>=1.2; extra == "dev"
27
+ Requires-Dist: matplotlib>=3.8; extra == "dev"
28
+ Requires-Dist: contextily<2,>=1.7.1; extra == "dev"
29
+ Requires-Dist: xyzservices<2027,>=2026.3; extra == "dev"
@@ -0,0 +1,29 @@
1
+ README.md
2
+ pyproject.toml
3
+ src/spatial_data_foundation.egg-info/PKG-INFO
4
+ src/spatial_data_foundation.egg-info/SOURCES.txt
5
+ src/spatial_data_foundation.egg-info/dependency_links.txt
6
+ src/spatial_data_foundation.egg-info/requires.txt
7
+ src/spatial_data_foundation.egg-info/top_level.txt
8
+ src/spatial_foundation/__init__.py
9
+ src/spatial_foundation/catalog.py
10
+ src/spatial_foundation/periods.py
11
+ src/spatial_foundation/geography/__init__.py
12
+ src/spatial_foundation/geography/gadm.py
13
+ src/spatial_foundation/geography/materialize.py
14
+ src/spatial_foundation/geography/membership.py
15
+ src/spatial_foundation/geography/models.py
16
+ src/spatial_foundation/geography/overlap.py
17
+ src/spatial_foundation/presentation/__init__.py
18
+ src/spatial_foundation/presentation/basemaps.py
19
+ src/spatial_foundation/presentation/plotting.py
20
+ tests/test_adversarial_spatial.py
21
+ tests/test_areal_overlap.py
22
+ tests/test_catalog.py
23
+ tests/test_distribution.py
24
+ tests/test_external_consumer.py
25
+ tests/test_gadm_materialization.py
26
+ tests/test_geography.py
27
+ tests/test_membership_kernel.py
28
+ tests/test_periods.py
29
+ tests/test_presentation.py
@@ -0,0 +1,29 @@
1
+ empirical-data-contracts<0.2,>=0.1
2
+ geopandas>=0.14
3
+ shapely>=2
4
+ pyproj>=3.5
5
+ pydantic<3,>=2.7
6
+
7
+ [cli]
8
+ typer>=0.12
9
+
10
+ [dev]
11
+ pytest>=8
12
+ ruff>=0.6
13
+ pyarrow>=15
14
+ build>=1.2
15
+ matplotlib>=3.8
16
+ contextily<2,>=1.7.1
17
+ xyzservices<2027,>=2026.3
18
+
19
+ [io]
20
+ pyarrow>=15
21
+ duckdb>=1.0
22
+
23
+ [presentation]
24
+ matplotlib>=3.8
25
+ contextily<2,>=1.7.1
26
+ xyzservices<2027,>=2026.3
27
+
28
+ [raster]
29
+ rasterio>=1.3
@@ -0,0 +1,20 @@
1
+ from .catalog import DataRoot, register_external_snapshot, sha256_file
2
+ from .geography import (
3
+ ArealOverlapAudit,
4
+ GADMMaterialization,
5
+ materialize_gadm,
6
+ relate_areal_objects,
7
+ )
8
+ from .periods import Period, PeriodIndex
9
+
10
+ __all__ = [
11
+ "ArealOverlapAudit",
12
+ "DataRoot",
13
+ "GADMMaterialization",
14
+ "Period",
15
+ "PeriodIndex",
16
+ "materialize_gadm",
17
+ "register_external_snapshot",
18
+ "relate_areal_objects",
19
+ "sha256_file",
20
+ ]
@@ -0,0 +1,52 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from hashlib import sha256
5
+ from pathlib import Path
6
+
7
+ from empirical_contracts import SourceFileRef, SourceSnapshotRef
8
+
9
+
10
+ @dataclass(frozen=True)
11
+ class DataRoot:
12
+ root: Path
13
+
14
+ @classmethod
15
+ def from_path(cls, value: str | Path) -> DataRoot:
16
+ return cls(Path(value).expanduser().resolve())
17
+
18
+ def bronze(self, source: str, release: str, snapshot_id: str) -> Path:
19
+ return self.root / "bronze" / source / release / snapshot_id
20
+
21
+ def silver(self, domain: str, dataset: str, version: str) -> Path:
22
+ return self.root / "silver" / domain / dataset / version
23
+
24
+ def gold(self, domain: str, dataset: str, version: str) -> Path:
25
+ return self.root / "gold" / domain / dataset / version
26
+
27
+ def run(self, package: str, run_id: str) -> Path:
28
+ return self.root / "runs" / package / run_id
29
+
30
+
31
+ def sha256_file(path: str | Path, chunk_size: int = 1024 * 1024) -> str:
32
+ digest = sha256()
33
+ with Path(path).open("rb") as handle:
34
+ while chunk := handle.read(chunk_size):
35
+ digest.update(chunk)
36
+ return digest.hexdigest()
37
+
38
+
39
+ def register_external_snapshot(source: str, release: str, paths: list[str | Path]) -> SourceSnapshotRef:
40
+ resolved_paths = sorted(Path(raw).expanduser().resolve() for raw in paths)
41
+ refs = []
42
+ for path in resolved_paths:
43
+ stat = path.stat()
44
+ refs.append(SourceFileRef(path=str(path), sha256=sha256_file(path), size_bytes=stat.st_size))
45
+ short = sha256("".join(ref.sha256 for ref in refs).encode()).hexdigest()[:12]
46
+ return SourceSnapshotRef(
47
+ source=source,
48
+ release=release,
49
+ snapshot_id=f"{source}-{release}-{short}",
50
+ storage_mode="external_immutable",
51
+ files=tuple(refs),
52
+ )
@@ -0,0 +1,19 @@
1
+ from .gadm import normalize_gadm_frame
2
+ from .materialize import GADMMaterialization, materialize_gadm
3
+ from .membership import MembershipAudit, assign_points
4
+ from .models import GeographyUnit, GeometryRole, MembershipStatus, geography_uid
5
+ from .overlap import ArealOverlapAudit, relate_areal_objects
6
+
7
+ __all__ = [
8
+ "ArealOverlapAudit",
9
+ "GADMMaterialization",
10
+ "GeographyUnit",
11
+ "GeometryRole",
12
+ "MembershipAudit",
13
+ "MembershipStatus",
14
+ "assign_points",
15
+ "geography_uid",
16
+ "materialize_gadm",
17
+ "normalize_gadm_frame",
18
+ "relate_areal_objects",
19
+ ]
@@ -0,0 +1,92 @@
1
+ from __future__ import annotations
2
+
3
+ import geopandas as gpd
4
+
5
+ from .models import GeometryRole, geography_uid
6
+
7
+ REQUIRED_BASE = {"GID_0"}
8
+
9
+
10
+ def _require_identity(series, *, label: str):
11
+ missing = series.isna() | series.astype("string").str.strip().eq("")
12
+ if missing.any():
13
+ raise ValueError(f"GADM {label} is not resolvable for {int(missing.sum())} row(s)")
14
+ return series.astype(str)
15
+
16
+
17
+ def normalize_gadm_frame(
18
+ frame: gpd.GeoDataFrame,
19
+ *,
20
+ version: str,
21
+ level: int,
22
+ area_crs: str = "EPSG:6933",
23
+ ) -> gpd.GeoDataFrame:
24
+ """Normalize one native GADM level without simplifying analytical geometry.
25
+
26
+ Expected source fields are GID_0 .. GID_{level}. Parent identity is retained when
27
+ available. Geometry is stored in EPSG:4326; area is calculated in the declared
28
+ equal-area CRS. The returned frame records the area CRS and geometry validity
29
+ profile in ``GeoDataFrame.attrs`` for run-manifest/provenance capture.
30
+ """
31
+ if level < 0:
32
+ raise ValueError("GADM admin level must be >= 0")
33
+ if not version.strip():
34
+ raise ValueError("GADM version must be non-empty")
35
+
36
+ gid_col = f"GID_{level}"
37
+ parent_col = f"GID_{level - 1}" if level > 0 else None
38
+ required = REQUIRED_BASE | {gid_col}
39
+ missing = required - set(frame.columns)
40
+ if missing:
41
+ raise ValueError(f"missing required GADM columns: {sorted(missing)}")
42
+ if frame.crs is None:
43
+ raise ValueError("GADM source must have a declared CRS")
44
+
45
+ source_ids = _require_identity(frame[gid_col], label="source geography identity")
46
+ country_ids = _require_identity(frame["GID_0"], label="country identity")
47
+
48
+ data = frame.to_crs("EPSG:4326").copy()
49
+ metric = data.to_crs(area_crs)
50
+ data["area_km2"] = metric.geometry.area / 1_000_000.0
51
+ data["provider"] = "gadm"
52
+ data["version"] = version
53
+ data["source_geo_id"] = source_ids.to_numpy()
54
+ data["country_iso3"] = country_ids.str.slice(0, 3).to_numpy()
55
+ data["native_admin_level"] = level
56
+ data["geo_uid"] = data["source_geo_id"].map(lambda x: geography_uid("gadm", version, level, x))
57
+ if parent_col and parent_col in data.columns:
58
+ parent_present = data[parent_col].notna() & data[parent_col].astype("string").str.strip().ne("")
59
+ data["parent_geo_uid"] = [
60
+ geography_uid("gadm", version, level - 1, str(value)) if present else None
61
+ for value, present in zip(data[parent_col], parent_present)
62
+ ]
63
+ else:
64
+ data["parent_geo_uid"] = None
65
+ data["geometry_role"] = GeometryRole.ANALYTICAL.value
66
+
67
+ keep = [
68
+ "geo_uid",
69
+ "provider",
70
+ "version",
71
+ "source_geo_id",
72
+ "country_iso3",
73
+ "native_admin_level",
74
+ "parent_geo_uid",
75
+ "area_km2",
76
+ "geometry_role",
77
+ "geometry",
78
+ ]
79
+ out = data[keep].copy()
80
+ if out["geo_uid"].duplicated().any():
81
+ dupes = out.loc[out["geo_uid"].duplicated(keep=False), "geo_uid"].unique().tolist()[:10]
82
+ raise ValueError(f"duplicate geography IDs after normalization: {dupes}")
83
+
84
+ null_geometry = out.geometry.isna()
85
+ invalid_geometry = ~null_geometry & ~out.geometry.is_valid
86
+ out.attrs["area_crs"] = area_crs
87
+ out.attrs["geometry_profile"] = {
88
+ "rows": len(out),
89
+ "null": int(null_geometry.sum()),
90
+ "invalid": int(invalid_geometry.sum()),
91
+ }
92
+ return out