kenya-data 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.
@@ -0,0 +1,30 @@
1
+ # Python
2
+ __pycache__/
3
+ *.pyc
4
+ .venv/
5
+ venv/
6
+ *.egg-info/
7
+ dist/
8
+ build/
9
+ .pytest_cache/
10
+ .mypy_cache/
11
+ .ruff_cache/
12
+
13
+ # Node / TypeScript
14
+ node_modules/
15
+ packages/typescript/dist/
16
+ packages/typescript/coverage/
17
+ npm-debug.log*
18
+
19
+ # Database (generated artifact — rebuilt via `make build-db`)
20
+ data/kenya.db
21
+ data/kenya.db-journal
22
+
23
+ # Editors / OS
24
+ .vscode/
25
+ .idea/
26
+ .DS_Store
27
+
28
+ # Env
29
+ .env
30
+ .env.local
@@ -0,0 +1,39 @@
1
+ Metadata-Version: 2.5
2
+ Name: kenya-data
3
+ Version: 0.1.0
4
+ Summary: Python SDK for the Kenya Data structured dataset
5
+ Project-URL: Homepage, https://github.com/ToshGitonga0/kenya-data
6
+ Project-URL: Repository, https://github.com/ToshGitonga0/kenya-data
7
+ Project-URL: Issues, https://github.com/ToshGitonga0/kenya-data/issues
8
+ Author: Kenya Data contributors
9
+ License: MIT
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python :: 3
13
+ Requires-Python: >=3.10
14
+ Provides-Extra: dev
15
+ Requires-Dist: mypy>=1.0; extra == 'dev'
16
+ Requires-Dist: pytest>=7.0; extra == 'dev'
17
+ Requires-Dist: ruff>=0.4; extra == 'dev'
18
+ Description-Content-Type: text/markdown
19
+
20
+ # kenya-data (Python SDK)
21
+
22
+ ```python
23
+ from kenya_data import Kenya
24
+
25
+ kenya = Kenya() # loads the bundled data/kenya.db by default
26
+ kenya = Kenya(db_path="/path/to/kenya.db") # or point at a specific build
27
+
28
+ kenya.counties.list()
29
+ kenya.counties.get("Nakuru")
30
+ kenya.constituencies.in_county("Nakuru")
31
+ kenya.wards.in_constituency("Naivasha")
32
+
33
+ kenya.dataset.version
34
+ kenya.dataset.updated_at
35
+ ```
36
+
37
+ Backed by the SQLite database built from `database/schema/schema.sql` —
38
+ see the repository root [README](../../README.md) and
39
+ [docs/sdk-design.md](../../docs/sdk-design.md).
@@ -0,0 +1,20 @@
1
+ # kenya-data (Python SDK)
2
+
3
+ ```python
4
+ from kenya_data import Kenya
5
+
6
+ kenya = Kenya() # loads the bundled data/kenya.db by default
7
+ kenya = Kenya(db_path="/path/to/kenya.db") # or point at a specific build
8
+
9
+ kenya.counties.list()
10
+ kenya.counties.get("Nakuru")
11
+ kenya.constituencies.in_county("Nakuru")
12
+ kenya.wards.in_constituency("Naivasha")
13
+
14
+ kenya.dataset.version
15
+ kenya.dataset.updated_at
16
+ ```
17
+
18
+ Backed by the SQLite database built from `database/schema/schema.sql` —
19
+ see the repository root [README](../../README.md) and
20
+ [docs/sdk-design.md](../../docs/sdk-design.md).
@@ -0,0 +1,42 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "kenya-data"
7
+ version = "0.1.0"
8
+ description = "Python SDK for the Kenya Data structured dataset"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "Kenya Data contributors" }]
13
+ classifiers = [
14
+ "Programming Language :: Python :: 3",
15
+ "License :: OSI Approved :: MIT License",
16
+ "Operating System :: OS Independent",
17
+ ]
18
+ dependencies = []
19
+
20
+ [project.urls]
21
+ Homepage = "https://github.com/ToshGitonga0/kenya-data"
22
+ Repository = "https://github.com/ToshGitonga0/kenya-data"
23
+ Issues = "https://github.com/ToshGitonga0/kenya-data/issues"
24
+
25
+ [project.optional-dependencies]
26
+ dev = ["pytest>=7.0", "mypy>=1.0", "ruff>=0.4"]
27
+
28
+ [tool.hatch.build.targets.wheel]
29
+ packages = ["src/kenya_data"]
30
+
31
+ [tool.hatch.build.targets.wheel.force-include]
32
+ "src/kenya_data/kenya.db" = "kenya_data/kenya.db"
33
+
34
+ [tool.pytest.ini_options]
35
+ testpaths = ["tests"]
36
+
37
+ [tool.mypy]
38
+ python_version = "3.10"
39
+ strict = true
40
+
41
+ [tool.ruff]
42
+ line-length = 100
@@ -0,0 +1,7 @@
1
+ """Kenya Data — Python SDK."""
2
+
3
+ from .client import Kenya
4
+ from .exceptions import EntityNotFoundError
5
+
6
+ __all__ = ["EntityNotFoundError", "Kenya"]
7
+ __version__ = "0.1.0"
@@ -0,0 +1,36 @@
1
+ """The top-level Kenya Data client."""
2
+ from __future__ import annotations
3
+
4
+ import sqlite3
5
+ from pathlib import Path
6
+
7
+ from .constituencies import Constituencies
8
+ from .counties import Counties
9
+ from .dataset import Dataset
10
+ from .db import connect
11
+ from .wards import Wards
12
+
13
+
14
+ class Kenya:
15
+ """Entry point for the Kenya Data SDK.
16
+
17
+ >>> kenya = Kenya()
18
+ >>> kenya.counties.list() # doctest: +SKIP
19
+ >>> kenya.counties.get("Nakuru") # doctest: +SKIP
20
+ """
21
+
22
+ def __init__(self, db_path: str | Path | None = None) -> None:
23
+ self._conn: sqlite3.Connection = connect(db_path)
24
+ self.counties = Counties(self._conn)
25
+ self.constituencies = Constituencies(self._conn)
26
+ self.wards = Wards(self._conn)
27
+ self.dataset = Dataset(self._conn)
28
+
29
+ def close(self) -> None:
30
+ self._conn.close()
31
+
32
+ def __enter__(self) -> Kenya: # noqa: PYI034 (Self requires Python >=3.11)
33
+ return self
34
+
35
+ def __exit__(self, *exc_info: object) -> None:
36
+ self.close()
@@ -0,0 +1,39 @@
1
+ """Constituency lookups."""
2
+ from __future__ import annotations
3
+
4
+ import builtins
5
+ import sqlite3
6
+
7
+ from .counties import Counties
8
+ from .exceptions import EntityNotFoundError
9
+ from .models import Constituency
10
+
11
+
12
+ class Constituencies:
13
+ def __init__(self, conn: sqlite3.Connection) -> None:
14
+ self._conn = conn
15
+
16
+ def list(self) -> builtins.list[Constituency]:
17
+ rows = self._conn.execute(
18
+ "SELECT id, code, name, county_id, status FROM constituencies ORDER BY name"
19
+ ).fetchall()
20
+ return [Constituency(**dict(r)) for r in rows]
21
+
22
+ def get(self, name_or_code: str) -> Constituency:
23
+ row = self._conn.execute(
24
+ "SELECT id, code, name, county_id, status FROM constituencies "
25
+ "WHERE name = ? COLLATE NOCASE OR code = ? COLLATE NOCASE",
26
+ (name_or_code, name_or_code),
27
+ ).fetchone()
28
+ if row is None:
29
+ raise EntityNotFoundError("constituency", name_or_code)
30
+ return Constituency(**dict(row))
31
+
32
+ def in_county(self, county_name_or_code: str) -> builtins.list[Constituency]:
33
+ county = Counties(self._conn).get(county_name_or_code)
34
+ rows = self._conn.execute(
35
+ "SELECT id, code, name, county_id, status FROM constituencies "
36
+ "WHERE county_id = ? ORDER BY name",
37
+ (county.id,),
38
+ ).fetchall()
39
+ return [Constituency(**dict(r)) for r in rows]
@@ -0,0 +1,29 @@
1
+ """County lookups."""
2
+ from __future__ import annotations
3
+
4
+ import builtins
5
+ import sqlite3
6
+
7
+ from .exceptions import EntityNotFoundError
8
+ from .models import County
9
+
10
+
11
+ class Counties:
12
+ def __init__(self, conn: sqlite3.Connection) -> None:
13
+ self._conn = conn
14
+
15
+ def list(self) -> builtins.list[County]:
16
+ rows = self._conn.execute(
17
+ "SELECT id, code, name, capital, status FROM counties ORDER BY name"
18
+ ).fetchall()
19
+ return [County(**dict(r)) for r in rows]
20
+
21
+ def get(self, name_or_code: str) -> County:
22
+ row = self._conn.execute(
23
+ "SELECT id, code, name, capital, status FROM counties "
24
+ "WHERE name = ? COLLATE NOCASE OR code = ? COLLATE NOCASE",
25
+ (name_or_code, name_or_code),
26
+ ).fetchone()
27
+ if row is None:
28
+ raise EntityNotFoundError("county", name_or_code)
29
+ return County(**dict(row))
@@ -0,0 +1,28 @@
1
+ """Dataset (as opposed to SDK) version metadata."""
2
+ from __future__ import annotations
3
+
4
+ import sqlite3
5
+
6
+ from .models import DatasetInfo
7
+
8
+
9
+ class Dataset:
10
+ def __init__(self, conn: sqlite3.Connection) -> None:
11
+ self._conn = conn
12
+
13
+ def info(self) -> DatasetInfo:
14
+ row = self._conn.execute(
15
+ "SELECT version, status, created_at FROM dataset_versions "
16
+ "ORDER BY id DESC LIMIT 1"
17
+ ).fetchone()
18
+ if row is None:
19
+ return DatasetInfo(version="unknown", status="unknown", updated_at="unknown")
20
+ return DatasetInfo(version=row["version"], status=row["status"], updated_at=row["created_at"])
21
+
22
+ @property
23
+ def version(self) -> str:
24
+ return self.info().version
25
+
26
+ @property
27
+ def updated_at(self) -> str:
28
+ return self.info().updated_at
@@ -0,0 +1,28 @@
1
+ """Connection handling and packaged database resolution."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sqlite3
6
+ from importlib.resources import files
7
+ from pathlib import Path
8
+
9
+
10
+ def default_db_path() -> Path:
11
+ packaged = Path(str(files("kenya_data").joinpath("kenya.db")))
12
+ if packaged.exists():
13
+ return packaged
14
+
15
+ # Local repository fallback.
16
+ return Path(__file__).resolve().parents[4] / "data" / "kenya.db"
17
+
18
+
19
+ def connect(db_path: str | Path | None = None) -> sqlite3.Connection:
20
+ path = Path(db_path) if db_path is not None else default_db_path()
21
+
22
+ if not path.exists():
23
+ raise FileNotFoundError(f"No Kenya Data database found at {path}")
24
+
25
+ conn = sqlite3.connect(path)
26
+ conn.row_factory = sqlite3.Row
27
+ conn.execute("PRAGMA foreign_keys = ON")
28
+ return conn
@@ -0,0 +1,14 @@
1
+ """Exceptions raised by the Kenya Data SDK."""
2
+
3
+
4
+ class KenyaDataError(Exception):
5
+ """Base class for all kenya-data SDK errors."""
6
+
7
+
8
+ class EntityNotFoundError(KenyaDataError):
9
+ """Raised when a lookup does not match any entity."""
10
+
11
+ def __init__(self, entity_type: str, identifier: str) -> None:
12
+ self.entity_type = entity_type
13
+ self.identifier = identifier
14
+ super().__init__(f"No {entity_type} found matching {identifier!r}")
Binary file
@@ -0,0 +1,38 @@
1
+ """Lightweight, typed record wrappers returned by the SDK."""
2
+ from __future__ import annotations
3
+
4
+ from dataclasses import dataclass
5
+
6
+
7
+ @dataclass(frozen=True)
8
+ class County:
9
+ id: int
10
+ code: str
11
+ name: str
12
+ capital: str | None
13
+ status: str
14
+
15
+
16
+ @dataclass(frozen=True)
17
+ class Constituency:
18
+ id: int
19
+ code: str
20
+ name: str
21
+ county_id: int
22
+ status: str
23
+
24
+
25
+ @dataclass(frozen=True)
26
+ class Ward:
27
+ id: int
28
+ code: str
29
+ name: str
30
+ constituency_id: int
31
+ status: str
32
+
33
+
34
+ @dataclass(frozen=True)
35
+ class DatasetInfo:
36
+ version: str
37
+ status: str
38
+ updated_at: str
@@ -0,0 +1,51 @@
1
+ """Ward lookups."""
2
+ from __future__ import annotations
3
+
4
+ import builtins
5
+ import sqlite3
6
+
7
+ from .constituencies import Constituencies
8
+ from .exceptions import EntityNotFoundError
9
+ from .models import Ward
10
+
11
+
12
+ class Wards:
13
+ def __init__(self, conn: sqlite3.Connection) -> None:
14
+ self._conn = conn
15
+
16
+ def list(self) -> builtins.list[Ward]:
17
+ rows = self._conn.execute(
18
+ "SELECT id, code, name, constituency_id, status FROM wards ORDER BY name"
19
+ ).fetchall()
20
+ return [Ward(**dict(r)) for r in rows]
21
+
22
+ def get(self, name_or_code: str) -> Ward:
23
+ row = self._conn.execute(
24
+ "SELECT id, code, name, constituency_id, status FROM wards "
25
+ "WHERE name = ? COLLATE NOCASE OR code = ? COLLATE NOCASE",
26
+ (name_or_code, name_or_code),
27
+ ).fetchone()
28
+ if row is None:
29
+ raise EntityNotFoundError("ward", name_or_code)
30
+ return Ward(**dict(row))
31
+
32
+ def in_constituency(self, constituency_name_or_code: str) -> builtins.list[Ward]:
33
+ constituency = Constituencies(self._conn).get(constituency_name_or_code)
34
+ rows = self._conn.execute(
35
+ "SELECT id, code, name, constituency_id, status FROM wards "
36
+ "WHERE constituency_id = ? ORDER BY name",
37
+ (constituency.id,),
38
+ ).fetchall()
39
+ return [Ward(**dict(r)) for r in rows]
40
+
41
+ def in_county(self, county_name_or_code: str) -> builtins.list[Ward]:
42
+ rows = self._conn.execute(
43
+ """SELECT wards.id, wards.code, wards.name, wards.constituency_id, wards.status
44
+ FROM wards
45
+ JOIN constituencies ON constituencies.id = wards.constituency_id
46
+ JOIN counties ON counties.id = constituencies.county_id
47
+ WHERE counties.name = ? COLLATE NOCASE OR counties.code = ? COLLATE NOCASE
48
+ ORDER BY wards.name""",
49
+ (county_name_or_code, county_name_or_code),
50
+ ).fetchall()
51
+ return [Ward(**dict(r)) for r in rows]
@@ -0,0 +1,18 @@
1
+ """Builds a throwaway SQLite database from the real approved data for each test session."""
2
+ from __future__ import annotations
3
+
4
+ import subprocess
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ import pytest
9
+
10
+ REPO_ROOT = Path(__file__).resolve().parents[3]
11
+
12
+
13
+ @pytest.fixture(scope="session")
14
+ def test_db_path(tmp_path_factory: pytest.TempPathFactory) -> Path:
15
+ db_path = tmp_path_factory.mktemp("kenya-data-db") / "kenya.db"
16
+ seed_script = REPO_ROOT / "database" / "seeds" / "load_approved_data.py"
17
+ subprocess.run([sys.executable, str(seed_script), str(db_path)], check=True)
18
+ return db_path
@@ -0,0 +1,57 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ import pytest
6
+
7
+ from kenya_data import EntityNotFoundError, Kenya
8
+
9
+
10
+ def test_counties_list_returns_real_counties(test_db_path: Path) -> None:
11
+ kenya = Kenya(db_path=test_db_path)
12
+ counties = kenya.counties.list()
13
+ assert len(counties) > 0
14
+ assert all(c.name for c in counties)
15
+
16
+
17
+ def test_counties_get_by_name_case_insensitive(test_db_path: Path) -> None:
18
+ kenya = Kenya(db_path=test_db_path)
19
+ county = kenya.counties.get("nakuru")
20
+ assert county.name.lower() == "nakuru"
21
+
22
+
23
+ def test_counties_get_unknown_raises(test_db_path: Path) -> None:
24
+ kenya = Kenya(db_path=test_db_path)
25
+ with pytest.raises(EntityNotFoundError):
26
+ kenya.counties.get("Not A Real County")
27
+
28
+
29
+ def test_constituencies_in_county(test_db_path: Path) -> None:
30
+ kenya = Kenya(db_path=test_db_path)
31
+ constituencies = kenya.constituencies.in_county("Nakuru")
32
+ assert len(constituencies) > 0
33
+ assert all(c.county_id == kenya.counties.get("Nakuru").id for c in constituencies)
34
+
35
+
36
+ def test_wards_in_constituency(test_db_path: Path) -> None:
37
+ kenya = Kenya(db_path=test_db_path)
38
+ constituency = kenya.constituencies.list()[0]
39
+ wards = kenya.wards.in_constituency(constituency.code)
40
+ assert all(w.constituency_id == constituency.id for w in wards)
41
+
42
+
43
+ def test_wards_in_county_traverses_hierarchy(test_db_path: Path) -> None:
44
+ kenya = Kenya(db_path=test_db_path)
45
+ wards = kenya.wards.in_county("Nakuru")
46
+ assert len(wards) > 0
47
+
48
+
49
+ def test_dataset_metadata_present(test_db_path: Path) -> None:
50
+ kenya = Kenya(db_path=test_db_path)
51
+ assert kenya.dataset.version
52
+ assert kenya.dataset.updated_at
53
+
54
+
55
+ def test_missing_database_raises_file_not_found(tmp_path: Path) -> None:
56
+ with pytest.raises(FileNotFoundError):
57
+ Kenya(db_path=tmp_path / "does-not-exist.db")