rwanda-geo-data 1.0.1__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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 LinkFy Connect
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,188 @@
1
+ Metadata-Version: 2.4
2
+ Name: rwanda-geo-data
3
+ Version: 1.0.1
4
+ Summary: Rwanda administrative divisions: provinces, districts, sectors, cells and villages.
5
+ Author: LinkFy Connect
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/megacodesdev/Rwanda-Geo-Data
8
+ Project-URL: Repository, https://github.com/megacodesdev/Rwanda-Geo-Data
9
+ Project-URL: Issues, https://github.com/megacodesdev/Rwanda-Geo-Data/issues
10
+ Keywords: rwanda,geo,provinces,districts,sectors,cells,villages
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Typing :: Typed
14
+ Requires-Python: >=3.8
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Dynamic: license-file
18
+
19
+ # Rwanda Geo Data
20
+
21
+ Rwanda's administrative divisions for JavaScript/TypeScript and Python:
22
+ **5 provinces → 30 districts → 416 sectors → 2,149 cells → 14,837 villages**.
23
+
24
+ Zero dependencies. Lookups are case-insensitive. District names are unique, so you only pass the district (no province) when looking up sectors, cells and villages.
25
+
26
+ ## JavaScript / TypeScript
27
+
28
+ ```bash
29
+ npm install @linkfy-connect/rwanda-geo-data
30
+ ```
31
+
32
+ Works in Node, and in any bundler-based framework (React, Next.js, Vue, Nuxt, Angular, Svelte/SvelteKit, Astro, Remix, React Native, Vite, webpack) via ESM, CommonJS and bundled TypeScript types.
33
+
34
+ ```js
35
+ import { getProvinces, getDistricts, getSectors, getCells, getVillages, search } from '@linkfy-connect/rwanda-geo-data';
36
+
37
+ getProvinces(); // ['East', 'Kigali', 'North', 'South', 'West']
38
+ getDistricts('East'); // ['Bugesera', 'Gatsibo', ...]
39
+ getSectors('Bugesera'); // ['Gashora', 'Juru', ...]
40
+ getCells('Bugesera', 'Gashora'); // ['Biryogo', 'Kabuye', ...]
41
+ getVillages('Bugesera', 'Gashora', 'Biryogo'); // ['Bidudu', 'Biryogo', ...]
42
+ ```
43
+
44
+ ### React: cascading dropdowns
45
+
46
+ ```jsx
47
+ import { useState } from 'react';
48
+ import { getProvinces, getDistricts, getSectors, getCells, getVillages } from '@linkfy-connect/rwanda-geo-data';
49
+
50
+ const LEVELS = ['province', 'district', 'sector', 'cell', 'village'];
51
+
52
+ export default function AddressPicker() {
53
+ const [a, setA] = useState({});
54
+
55
+ // Changing a level clears every level below it.
56
+ const choose = (level, value) => {
57
+ const keep = LEVELS.slice(0, LEVELS.indexOf(level));
58
+ setA({ ...Object.fromEntries(keep.map((k) => [k, a[k]])), [level]: value });
59
+ };
60
+
61
+ const options = {
62
+ province: getProvinces(),
63
+ district: a.province ? getDistricts(a.province) : [],
64
+ sector: a.district ? getSectors(a.district) : [],
65
+ cell: a.sector ? getCells(a.district, a.sector) : [],
66
+ village: a.cell ? getVillages(a.district, a.sector, a.cell) : [],
67
+ };
68
+
69
+ return LEVELS.map((level) => (
70
+ <select key={level} value={a[level] || ''} disabled={!options[level].length}
71
+ onChange={(e) => choose(level, e.target.value)}>
72
+ <option value="">Select {level}</option>
73
+ {options[level].map((o) => <option key={o}>{o}</option>)}
74
+ </select>
75
+ ));
76
+ }
77
+ ```
78
+
79
+ ### Vue 3
80
+
81
+ ```vue
82
+ <script setup>
83
+ import { ref, computed } from 'vue';
84
+ import { getProvinces, getDistricts } from '@linkfy-connect/rwanda-geo-data';
85
+ const province = ref('');
86
+ const districts = computed(() => getDistricts(province.value));
87
+ </script>
88
+ <template>
89
+ <select v-model="province"><option v-for="p in getProvinces()" :key="p">{{ p }}</option></select>
90
+ <select><option v-for="d in districts" :key="d">{{ d }}</option></select>
91
+ </template>
92
+ ```
93
+
94
+ ### Angular, Svelte, Next.js, Nuxt
95
+
96
+ Same import everywhere: `import { getDistricts } from '@linkfy-connect/rwanda-geo-data'`. In Next.js/Nuxt it also works in server code (API routes, server components).
97
+
98
+ The whole dataset is about 190 kB in the bundle (roughly 40 kB gzipped). To keep it out of the initial bundle, load it lazily:
99
+
100
+ ```js
101
+ const geo = await import('@linkfy-connect/rwanda-geo-data');
102
+ ```
103
+
104
+ ### API
105
+
106
+ | Function | Returns |
107
+ |---|---|
108
+ | `getProvinces()` | `string[]` |
109
+ | `getDistricts(province?)` | districts of a province, or all 30 |
110
+ | `getProvinceOf(district)` | province name or `undefined` |
111
+ | `getSectors(district)` | `string[]` |
112
+ | `getCells(district, sector)` | `string[]` |
113
+ | `getVillages(district, sector, cell)` | `string[]` |
114
+ | `isValidAddress({province, district, sector?, cell?, village?})` | `boolean` |
115
+ | `search(query, {levels?, limit?})` | `[{level, name, province, district?, ...}]` |
116
+ | `getStats()` | counts per level |
117
+ | `data` | the raw nested object |
118
+
119
+ ## Python
120
+
121
+ ```bash
122
+ pip install rwanda-geo-data
123
+ ```
124
+
125
+ Python 3.8+, standard library only.
126
+
127
+ ```python
128
+ import rwanda_geo_data as geo
129
+
130
+ geo.get_provinces() # ['East', 'Kigali', 'North', 'South', 'West']
131
+ geo.get_districts("East") # ['Bugesera', 'Gatsibo', ...]
132
+ geo.get_sectors("Bugesera") # ['Gashora', 'Juru', ...]
133
+ geo.get_cells("Bugesera", "Gashora") # ['Biryogo', 'Kabuye', ...]
134
+ geo.get_villages("Bugesera", "Gashora", "Biryogo") # ['Bidudu', 'Biryogo', ...]
135
+ geo.is_valid_address("East", "Bugesera", "Gashora") # True
136
+ geo.search("gashora", levels=["sector"])
137
+ geo.DATA # raw nested dict
138
+ ```
139
+
140
+ ### FastAPI
141
+
142
+ ```python
143
+ from fastapi import FastAPI
144
+ import rwanda_geo_data as geo
145
+
146
+ app = FastAPI()
147
+
148
+ @app.get("/districts/{province}")
149
+ def districts(province: str):
150
+ return geo.get_districts(province)
151
+
152
+ @app.get("/sectors/{district}")
153
+ def sectors(district: str):
154
+ return geo.get_sectors(district)
155
+ ```
156
+
157
+ ### Django / Flask
158
+
159
+ Call the same functions inside any view, serializer or form validator, for example `geo.is_valid_address(...)` in a form's `clean()`.
160
+
161
+ ## Development
162
+
163
+ ```bash
164
+ npm test # builds dist/, syncs data to python/, runs JS tests
165
+ cd python && python -m unittest discover -s tests
166
+ ```
167
+
168
+ `data/rwanda-geo-data.json` is the single source of truth. Edit it, then run `npm run build`: this regenerates `dist/` and copies the data into the Python package.
169
+
170
+ ## Publishing
171
+
172
+ ```bash
173
+ # npm
174
+ npm login
175
+ npm publish # use `npm publish --access public` for a scoped name
176
+
177
+ # PyPI
178
+ cd python
179
+ pip install build twine
180
+ python -m build
181
+ twine upload dist/*
182
+ ```
183
+
184
+ Bump the version in both `package.json` and `python/pyproject.toml` for each release.
185
+
186
+ ## License
187
+
188
+ MIT
@@ -0,0 +1,170 @@
1
+ # Rwanda Geo Data
2
+
3
+ Rwanda's administrative divisions for JavaScript/TypeScript and Python:
4
+ **5 provinces → 30 districts → 416 sectors → 2,149 cells → 14,837 villages**.
5
+
6
+ Zero dependencies. Lookups are case-insensitive. District names are unique, so you only pass the district (no province) when looking up sectors, cells and villages.
7
+
8
+ ## JavaScript / TypeScript
9
+
10
+ ```bash
11
+ npm install @linkfy-connect/rwanda-geo-data
12
+ ```
13
+
14
+ Works in Node, and in any bundler-based framework (React, Next.js, Vue, Nuxt, Angular, Svelte/SvelteKit, Astro, Remix, React Native, Vite, webpack) via ESM, CommonJS and bundled TypeScript types.
15
+
16
+ ```js
17
+ import { getProvinces, getDistricts, getSectors, getCells, getVillages, search } from '@linkfy-connect/rwanda-geo-data';
18
+
19
+ getProvinces(); // ['East', 'Kigali', 'North', 'South', 'West']
20
+ getDistricts('East'); // ['Bugesera', 'Gatsibo', ...]
21
+ getSectors('Bugesera'); // ['Gashora', 'Juru', ...]
22
+ getCells('Bugesera', 'Gashora'); // ['Biryogo', 'Kabuye', ...]
23
+ getVillages('Bugesera', 'Gashora', 'Biryogo'); // ['Bidudu', 'Biryogo', ...]
24
+ ```
25
+
26
+ ### React: cascading dropdowns
27
+
28
+ ```jsx
29
+ import { useState } from 'react';
30
+ import { getProvinces, getDistricts, getSectors, getCells, getVillages } from '@linkfy-connect/rwanda-geo-data';
31
+
32
+ const LEVELS = ['province', 'district', 'sector', 'cell', 'village'];
33
+
34
+ export default function AddressPicker() {
35
+ const [a, setA] = useState({});
36
+
37
+ // Changing a level clears every level below it.
38
+ const choose = (level, value) => {
39
+ const keep = LEVELS.slice(0, LEVELS.indexOf(level));
40
+ setA({ ...Object.fromEntries(keep.map((k) => [k, a[k]])), [level]: value });
41
+ };
42
+
43
+ const options = {
44
+ province: getProvinces(),
45
+ district: a.province ? getDistricts(a.province) : [],
46
+ sector: a.district ? getSectors(a.district) : [],
47
+ cell: a.sector ? getCells(a.district, a.sector) : [],
48
+ village: a.cell ? getVillages(a.district, a.sector, a.cell) : [],
49
+ };
50
+
51
+ return LEVELS.map((level) => (
52
+ <select key={level} value={a[level] || ''} disabled={!options[level].length}
53
+ onChange={(e) => choose(level, e.target.value)}>
54
+ <option value="">Select {level}</option>
55
+ {options[level].map((o) => <option key={o}>{o}</option>)}
56
+ </select>
57
+ ));
58
+ }
59
+ ```
60
+
61
+ ### Vue 3
62
+
63
+ ```vue
64
+ <script setup>
65
+ import { ref, computed } from 'vue';
66
+ import { getProvinces, getDistricts } from '@linkfy-connect/rwanda-geo-data';
67
+ const province = ref('');
68
+ const districts = computed(() => getDistricts(province.value));
69
+ </script>
70
+ <template>
71
+ <select v-model="province"><option v-for="p in getProvinces()" :key="p">{{ p }}</option></select>
72
+ <select><option v-for="d in districts" :key="d">{{ d }}</option></select>
73
+ </template>
74
+ ```
75
+
76
+ ### Angular, Svelte, Next.js, Nuxt
77
+
78
+ Same import everywhere: `import { getDistricts } from '@linkfy-connect/rwanda-geo-data'`. In Next.js/Nuxt it also works in server code (API routes, server components).
79
+
80
+ The whole dataset is about 190 kB in the bundle (roughly 40 kB gzipped). To keep it out of the initial bundle, load it lazily:
81
+
82
+ ```js
83
+ const geo = await import('@linkfy-connect/rwanda-geo-data');
84
+ ```
85
+
86
+ ### API
87
+
88
+ | Function | Returns |
89
+ |---|---|
90
+ | `getProvinces()` | `string[]` |
91
+ | `getDistricts(province?)` | districts of a province, or all 30 |
92
+ | `getProvinceOf(district)` | province name or `undefined` |
93
+ | `getSectors(district)` | `string[]` |
94
+ | `getCells(district, sector)` | `string[]` |
95
+ | `getVillages(district, sector, cell)` | `string[]` |
96
+ | `isValidAddress({province, district, sector?, cell?, village?})` | `boolean` |
97
+ | `search(query, {levels?, limit?})` | `[{level, name, province, district?, ...}]` |
98
+ | `getStats()` | counts per level |
99
+ | `data` | the raw nested object |
100
+
101
+ ## Python
102
+
103
+ ```bash
104
+ pip install rwanda-geo-data
105
+ ```
106
+
107
+ Python 3.8+, standard library only.
108
+
109
+ ```python
110
+ import rwanda_geo_data as geo
111
+
112
+ geo.get_provinces() # ['East', 'Kigali', 'North', 'South', 'West']
113
+ geo.get_districts("East") # ['Bugesera', 'Gatsibo', ...]
114
+ geo.get_sectors("Bugesera") # ['Gashora', 'Juru', ...]
115
+ geo.get_cells("Bugesera", "Gashora") # ['Biryogo', 'Kabuye', ...]
116
+ geo.get_villages("Bugesera", "Gashora", "Biryogo") # ['Bidudu', 'Biryogo', ...]
117
+ geo.is_valid_address("East", "Bugesera", "Gashora") # True
118
+ geo.search("gashora", levels=["sector"])
119
+ geo.DATA # raw nested dict
120
+ ```
121
+
122
+ ### FastAPI
123
+
124
+ ```python
125
+ from fastapi import FastAPI
126
+ import rwanda_geo_data as geo
127
+
128
+ app = FastAPI()
129
+
130
+ @app.get("/districts/{province}")
131
+ def districts(province: str):
132
+ return geo.get_districts(province)
133
+
134
+ @app.get("/sectors/{district}")
135
+ def sectors(district: str):
136
+ return geo.get_sectors(district)
137
+ ```
138
+
139
+ ### Django / Flask
140
+
141
+ Call the same functions inside any view, serializer or form validator, for example `geo.is_valid_address(...)` in a form's `clean()`.
142
+
143
+ ## Development
144
+
145
+ ```bash
146
+ npm test # builds dist/, syncs data to python/, runs JS tests
147
+ cd python && python -m unittest discover -s tests
148
+ ```
149
+
150
+ `data/rwanda-geo-data.json` is the single source of truth. Edit it, then run `npm run build`: this regenerates `dist/` and copies the data into the Python package.
151
+
152
+ ## Publishing
153
+
154
+ ```bash
155
+ # npm
156
+ npm login
157
+ npm publish # use `npm publish --access public` for a scoped name
158
+
159
+ # PyPI
160
+ cd python
161
+ pip install build twine
162
+ python -m build
163
+ twine upload dist/*
164
+ ```
165
+
166
+ Bump the version in both `package.json` and `python/pyproject.toml` for each release.
167
+
168
+ ## License
169
+
170
+ MIT
@@ -0,0 +1,29 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "rwanda-geo-data"
7
+ version = "1.0.1"
8
+ description = "Rwanda administrative divisions: provinces, districts, sectors, cells and villages."
9
+ readme = "README.md"
10
+ requires-python = ">=3.8"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "LinkFy Connect" }]
13
+ keywords = ["rwanda", "geo", "provinces", "districts", "sectors", "cells", "villages"]
14
+ classifiers = [
15
+ "Programming Language :: Python :: 3",
16
+ "License :: OSI Approved :: MIT License",
17
+ "Typing :: Typed",
18
+ ]
19
+
20
+ [project.urls]
21
+ Homepage = "https://github.com/megacodesdev/Rwanda-Geo-Data"
22
+ Repository = "https://github.com/megacodesdev/Rwanda-Geo-Data"
23
+ Issues = "https://github.com/megacodesdev/Rwanda-Geo-Data/issues"
24
+
25
+ [tool.setuptools.packages.find]
26
+ where = ["src"]
27
+
28
+ [tool.setuptools.package-data]
29
+ rwanda_geo_data = ["data.json", "py.typed"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,165 @@
1
+ """Rwanda administrative divisions: province > district > sector > cell > villages.
2
+
3
+ All lookups are case-insensitive. District names are unique nationwide, so
4
+ lookups below province only need the district.
5
+ """
6
+ import json
7
+ from pathlib import Path
8
+ from typing import Any, Dict, List, Optional
9
+
10
+ __all__ = [
11
+ "DATA", "get_provinces", "get_districts", "get_province_of", "get_sectors",
12
+ "get_cells", "get_villages", "is_valid_address", "search", "get_stats",
13
+ ]
14
+
15
+ with open(Path(__file__).with_name("data.json"), encoding="utf-8") as _f:
16
+ DATA: Dict[str, Dict[str, Dict[str, Dict[str, List[str]]]]] = json.load(_f)
17
+
18
+
19
+ def _norm(s: Any) -> str:
20
+ return "" if s is None else str(s).strip().lower()
21
+
22
+
23
+ def _find_key(obj: Optional[Dict[str, Any]], name: Any) -> Optional[str]:
24
+ if not obj:
25
+ return None
26
+ n = _norm(name)
27
+ for k in obj:
28
+ if _norm(k) == n:
29
+ return k
30
+ return None
31
+
32
+
33
+ _DISTRICT_INDEX: Dict[str, str] = {
34
+ _norm(d): p for p, ds in DATA.items() for d in ds
35
+ }
36
+
37
+
38
+ def get_provinces() -> List[str]:
39
+ return list(DATA)
40
+
41
+
42
+ def get_districts(province: Optional[str] = None) -> List[str]:
43
+ if province is None:
44
+ return [d for ds in DATA.values() for d in ds]
45
+ p = _find_key(DATA, province)
46
+ return list(DATA[p]) if p else []
47
+
48
+
49
+ def get_province_of(district: str) -> Optional[str]:
50
+ return _DISTRICT_INDEX.get(_norm(district))
51
+
52
+
53
+ def get_sectors(district: str) -> List[str]:
54
+ p = get_province_of(district)
55
+ if not p:
56
+ return []
57
+ d = _find_key(DATA[p], district)
58
+ return list(DATA[p][d])
59
+
60
+
61
+ def get_cells(district: str, sector: str) -> List[str]:
62
+ p = get_province_of(district)
63
+ if not p:
64
+ return []
65
+ d = _find_key(DATA[p], district)
66
+ s = _find_key(DATA[p][d], sector)
67
+ return list(DATA[p][d][s]) if s else []
68
+
69
+
70
+ def get_villages(district: str, sector: str, cell: str) -> List[str]:
71
+ p = get_province_of(district)
72
+ if not p:
73
+ return []
74
+ d = _find_key(DATA[p], district)
75
+ s = _find_key(DATA[p][d], sector)
76
+ if not s:
77
+ return []
78
+ c = _find_key(DATA[p][d][s], cell)
79
+ return list(DATA[p][d][s][c]) if c else []
80
+
81
+
82
+ def is_valid_address(
83
+ province: str,
84
+ district: str,
85
+ sector: Optional[str] = None,
86
+ cell: Optional[str] = None,
87
+ village: Optional[str] = None,
88
+ ) -> bool:
89
+ p = _find_key(DATA, province)
90
+ if not p:
91
+ return False
92
+ d = _find_key(DATA[p], district)
93
+ if not d:
94
+ return False
95
+ if sector is None:
96
+ return True
97
+ s = _find_key(DATA[p][d], sector)
98
+ if not s:
99
+ return False
100
+ if cell is None:
101
+ return True
102
+ c = _find_key(DATA[p][d][s], cell)
103
+ if not c:
104
+ return False
105
+ if village is None:
106
+ return True
107
+ return any(_norm(v) == _norm(village) for v in DATA[p][d][s][c])
108
+
109
+
110
+ def search(
111
+ query: str,
112
+ levels: Optional[List[str]] = None,
113
+ limit: int = 50,
114
+ ) -> List[Dict[str, str]]:
115
+ """Case-insensitive substring search. Levels: province, district, sector, cell, village."""
116
+ q = _norm(query)
117
+ if not q:
118
+ return []
119
+ want = set(levels or ["province", "district", "sector", "cell", "village"])
120
+ out: List[Dict[str, str]] = []
121
+
122
+ def push(r: Dict[str, str]) -> bool:
123
+ out.append(r)
124
+ return len(out) >= limit
125
+
126
+ for province, districts in DATA.items():
127
+ if "province" in want and q in _norm(province):
128
+ if push({"level": "province", "name": province, "province": province}):
129
+ return out
130
+ for district, sectors in districts.items():
131
+ if "district" in want and q in _norm(district):
132
+ if push({"level": "district", "name": district, "province": province, "district": district}):
133
+ return out
134
+ for sector, cells in sectors.items():
135
+ if "sector" in want and q in _norm(sector):
136
+ if push({"level": "sector", "name": sector, "province": province,
137
+ "district": district, "sector": sector}):
138
+ return out
139
+ for cell, villages in cells.items():
140
+ if "cell" in want and q in _norm(cell):
141
+ if push({"level": "cell", "name": cell, "province": province,
142
+ "district": district, "sector": sector, "cell": cell}):
143
+ return out
144
+ if "village" in want:
145
+ for village in villages:
146
+ if q in _norm(village):
147
+ if push({"level": "village", "name": village, "province": province,
148
+ "district": district, "sector": sector,
149
+ "cell": cell, "village": village}):
150
+ return out
151
+ return out
152
+
153
+
154
+ def get_stats() -> Dict[str, int]:
155
+ districts = sectors = cells = villages = 0
156
+ for ds in DATA.values():
157
+ for ss in ds.values():
158
+ districts += 1
159
+ for cs in ss.values():
160
+ sectors += 1
161
+ for vs in cs.values():
162
+ cells += 1
163
+ villages += len(vs)
164
+ return {"provinces": len(DATA), "districts": districts, "sectors": sectors,
165
+ "cells": cells, "villages": villages}