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.
- rwanda_geo_data-1.0.1/LICENSE +21 -0
- rwanda_geo_data-1.0.1/PKG-INFO +188 -0
- rwanda_geo_data-1.0.1/README.md +170 -0
- rwanda_geo_data-1.0.1/pyproject.toml +29 -0
- rwanda_geo_data-1.0.1/setup.cfg +4 -0
- rwanda_geo_data-1.0.1/src/rwanda_geo_data/__init__.py +165 -0
- rwanda_geo_data-1.0.1/src/rwanda_geo_data/data.json +16505 -0
- rwanda_geo_data-1.0.1/src/rwanda_geo_data/py.typed +0 -0
- rwanda_geo_data-1.0.1/src/rwanda_geo_data.egg-info/PKG-INFO +188 -0
- rwanda_geo_data-1.0.1/src/rwanda_geo_data.egg-info/SOURCES.txt +11 -0
- rwanda_geo_data-1.0.1/src/rwanda_geo_data.egg-info/dependency_links.txt +1 -0
- rwanda_geo_data-1.0.1/src/rwanda_geo_data.egg-info/top_level.txt +1 -0
- rwanda_geo_data-1.0.1/tests/test_geo.py +34 -0
|
@@ -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,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}
|