afrogeo 0.1.0__py3-none-any.whl
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.
- afrogeo/__init__.py +20 -0
- afrogeo/data/__init__.py +0 -0
- afrogeo/result.py +33 -0
- afrogeo/verify.py +152 -0
- afrogeo-0.1.0.dist-info/METADATA +108 -0
- afrogeo-0.1.0.dist-info/RECORD +9 -0
- afrogeo-0.1.0.dist-info/WHEEL +5 -0
- afrogeo-0.1.0.dist-info/licenses/LICENSE +21 -0
- afrogeo-0.1.0.dist-info/top_level.txt +1 -0
afrogeo/__init__.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Afrogeo: Offline-first African location verification for Python.
|
|
3
|
+
|
|
4
|
+
This package provides tools to validate and normalize location data
|
|
5
|
+
(Country, State, LGA, City) across Africa using local JSON datasets.
|
|
6
|
+
It is designed to work without an internet connection.
|
|
7
|
+
|
|
8
|
+
Main function:
|
|
9
|
+
verify: Validates location data and returns a Result object.
|
|
10
|
+
|
|
11
|
+
Usage:
|
|
12
|
+
>>> from afrogeo import verify
|
|
13
|
+
>>> result = verify({"country": "Nigeria", "state": "Lagos", "lga": "Agege"})
|
|
14
|
+
>>> if result:
|
|
15
|
+
... print("Location is valid!")
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from .verify import verify
|
|
19
|
+
|
|
20
|
+
__all__ = ["verify"]
|
afrogeo/data/__init__.py
ADDED
|
File without changes
|
afrogeo/result.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
class Result:
|
|
2
|
+
"""
|
|
3
|
+
Represents the outcome of a location verification.
|
|
4
|
+
|
|
5
|
+
This object is returned by the `verify()` function and contains
|
|
6
|
+
information about whether the validation succeeded, any errors
|
|
7
|
+
encountered, and the normalized location data.
|
|
8
|
+
|
|
9
|
+
Attributes:
|
|
10
|
+
valid (bool): True if the location was successfully verified.
|
|
11
|
+
errors (list[str]): A list of error messages if validation failed.
|
|
12
|
+
normalized (dict): A dictionary containing standardized location names
|
|
13
|
+
(country, state, lga, city) and codes if successful.
|
|
14
|
+
"""
|
|
15
|
+
def __init__(self, valid: bool, errors=None, normalized=None):
|
|
16
|
+
"""
|
|
17
|
+
Initialize a Result object.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
valid: Boolean indicating success.
|
|
21
|
+
errors: Optional list of error strings.
|
|
22
|
+
normalized: Optional dictionary of normalized data.
|
|
23
|
+
"""
|
|
24
|
+
self.valid = valid
|
|
25
|
+
self.errors = errors or []
|
|
26
|
+
self.normalized = normalized or {}
|
|
27
|
+
|
|
28
|
+
def __bool__(self):
|
|
29
|
+
"""Allows using the result directly in an 'if' statement."""
|
|
30
|
+
return self.valid
|
|
31
|
+
|
|
32
|
+
def __repr__(self):
|
|
33
|
+
return f"<AfrogeoResult valid={self.valid} errors={self.errors}>"
|
afrogeo/verify.py
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import functools
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from afrogeo.result import Result
|
|
5
|
+
|
|
6
|
+
# Constants
|
|
7
|
+
BASE_DIR = Path(__file__).parent
|
|
8
|
+
DATA_DIR = BASE_DIR / "data"
|
|
9
|
+
|
|
10
|
+
@functools.lru_cache(maxsize=10)
|
|
11
|
+
def load_country_data(country_code: str):
|
|
12
|
+
"""
|
|
13
|
+
Load country data from a JSON file in the data directory.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
country_code: The ISO country code (e.g., 'ng').
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
A dictionary containing the country data, or None if not found.
|
|
20
|
+
"""
|
|
21
|
+
file_path = DATA_DIR / f"{country_code.lower()}.json"
|
|
22
|
+
if not file_path.exists():
|
|
23
|
+
return None
|
|
24
|
+
|
|
25
|
+
try:
|
|
26
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
|
27
|
+
return json.load(f)
|
|
28
|
+
except (json.JSONDecodeError, IOError):
|
|
29
|
+
return None
|
|
30
|
+
|
|
31
|
+
def verify(user_input: dict):
|
|
32
|
+
"""
|
|
33
|
+
Verify African location data against local datasets.
|
|
34
|
+
|
|
35
|
+
This function validates if a given state, LGA, and optional city exist
|
|
36
|
+
within the specified country. It handles case-insensitivity and
|
|
37
|
+
normalizes the output.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
user_input (dict): A dictionary containing:
|
|
41
|
+
- 'country' (str): Full name or ISO code (e.g., 'Nigeria' or 'NG').
|
|
42
|
+
- 'state' (str): Full name or code (e.g., 'Lagos' or 'LA').
|
|
43
|
+
- 'lga' (str): Local Government Area name.
|
|
44
|
+
- 'city' (str, optional): City name for validation.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
afrogeo.result.Result: An object containing validation status,
|
|
48
|
+
errors (if any), and normalized data.
|
|
49
|
+
|
|
50
|
+
Example:
|
|
51
|
+
>>> from afrogeo import verify
|
|
52
|
+
>>> res = verify({"country": "NG", "state": "LA", "lga": "Agege"})
|
|
53
|
+
>>> if res:
|
|
54
|
+
... print(res.normalized['state'])
|
|
55
|
+
Lagos
|
|
56
|
+
"""
|
|
57
|
+
country_input = str(user_input.get("country", "")).strip().lower()
|
|
58
|
+
state_input = str(user_input.get("state", "")).strip().lower()
|
|
59
|
+
lga_input = str(user_input.get("lga", "")).strip().lower()
|
|
60
|
+
city_input = str(user_input.get("city", "")).strip().lower() or None
|
|
61
|
+
|
|
62
|
+
if not country_input:
|
|
63
|
+
return Result(False, ["country is required"])
|
|
64
|
+
if not state_input or not lga_input:
|
|
65
|
+
return Result(False, ["state and lga are required"])
|
|
66
|
+
|
|
67
|
+
# -------------------------
|
|
68
|
+
# DATA LOADING (Dynamic)
|
|
69
|
+
# -------------------------
|
|
70
|
+
# Map country names to codes if necessary (Basic mapping for now)
|
|
71
|
+
country_map = {"nigeria": "ng", "ghana": "gh", "kenya": "ke"}
|
|
72
|
+
country_code = country_map.get(country_input, country_input)
|
|
73
|
+
|
|
74
|
+
data = load_country_data(country_code)
|
|
75
|
+
if not data:
|
|
76
|
+
return Result(False, [f"Data for country '{country_input}' not found or unsupported"])
|
|
77
|
+
|
|
78
|
+
# -------------------------
|
|
79
|
+
# COUNTRY MATCH
|
|
80
|
+
# -------------------------
|
|
81
|
+
found_country_key = None
|
|
82
|
+
for key in data.keys():
|
|
83
|
+
if key.lower() == country_code:
|
|
84
|
+
found_country_key = key
|
|
85
|
+
break
|
|
86
|
+
|
|
87
|
+
if not found_country_key:
|
|
88
|
+
return Result(False, [f"Invalid country code in data for '{country_input}'"])
|
|
89
|
+
|
|
90
|
+
country_data = data[found_country_key]
|
|
91
|
+
states = country_data.get("states", {})
|
|
92
|
+
|
|
93
|
+
# -------------------------
|
|
94
|
+
# STATE MATCH (code OR name)
|
|
95
|
+
# -------------------------
|
|
96
|
+
found_state_code = None
|
|
97
|
+
found_state_data = None
|
|
98
|
+
|
|
99
|
+
for code, state in states.items():
|
|
100
|
+
if (
|
|
101
|
+
code.lower() == state_input
|
|
102
|
+
or state["name"].lower() == state_input
|
|
103
|
+
):
|
|
104
|
+
found_state_code = code
|
|
105
|
+
found_state_data = state
|
|
106
|
+
break
|
|
107
|
+
|
|
108
|
+
if not found_state_code:
|
|
109
|
+
return Result(False, ["Invalid state"])
|
|
110
|
+
|
|
111
|
+
# -------------------------
|
|
112
|
+
# LGA MATCH
|
|
113
|
+
# -------------------------
|
|
114
|
+
lgas = found_state_data.get("lgas", {})
|
|
115
|
+
|
|
116
|
+
found_lga_name = None
|
|
117
|
+
found_lga_info = None
|
|
118
|
+
|
|
119
|
+
for lga_name, lga_info in lgas.items():
|
|
120
|
+
if lga_name.lower() == lga_input:
|
|
121
|
+
found_lga_name = lga_name
|
|
122
|
+
found_lga_info = lga_info
|
|
123
|
+
break
|
|
124
|
+
|
|
125
|
+
if not found_lga_name:
|
|
126
|
+
return Result(False, ["Invalid LGA for selected state"])
|
|
127
|
+
|
|
128
|
+
# -------------------------
|
|
129
|
+
# OPTIONAL CITY CHECK
|
|
130
|
+
# -------------------------
|
|
131
|
+
found_city_name = found_lga_info.get("city")
|
|
132
|
+
if city_input:
|
|
133
|
+
if not found_city_name:
|
|
134
|
+
return Result(False, ["LGA does not belong to a city"])
|
|
135
|
+
|
|
136
|
+
if found_city_name.lower() != city_input:
|
|
137
|
+
return Result(False, ["City does not match LGA"])
|
|
138
|
+
|
|
139
|
+
# -------------------------
|
|
140
|
+
# SUCCESS
|
|
141
|
+
# -------------------------
|
|
142
|
+
normalized = {
|
|
143
|
+
"country_code": found_country_key,
|
|
144
|
+
"country": country_data["name"],
|
|
145
|
+
"state_code": found_state_code,
|
|
146
|
+
"state": found_state_data["name"],
|
|
147
|
+
"capital": found_state_data.get("capital"),
|
|
148
|
+
"lga": found_lga_name,
|
|
149
|
+
"city": found_city_name,
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return Result(True, [], normalized)
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: afrogeo
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Offline-first African location verification for Python.
|
|
5
|
+
Author: 404Codes
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 404 Codes
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Requires-Python: >=3.8
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
License-File: LICENSE
|
|
31
|
+
Provides-Extra: dev
|
|
32
|
+
Requires-Dist: pytest; extra == "dev"
|
|
33
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
34
|
+
Dynamic: license-file
|
|
35
|
+
|
|
36
|
+
# Afrogeo
|
|
37
|
+
|
|
38
|
+
Offline-first African location verification for Python.
|
|
39
|
+
|
|
40
|
+
Validate country, state, city, and district (LGA) without an internet connection.
|
|
41
|
+
|
|
42
|
+
## Installation
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pip install afrogeo
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Usage
|
|
49
|
+
|
|
50
|
+
Afrogeo provides a simple `verify` function that takes a dictionary and returns a `Result` object.
|
|
51
|
+
|
|
52
|
+
### Basic Example
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from afrogeo import verify
|
|
56
|
+
|
|
57
|
+
data = {
|
|
58
|
+
"country": "Nigeria",
|
|
59
|
+
"state": "Lagos",
|
|
60
|
+
"lga": "Agege"
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
result = verify(data)
|
|
64
|
+
|
|
65
|
+
if result.valid:
|
|
66
|
+
print(f"Validated: {result.normalized['state']} - {result.normalized['lga']}")
|
|
67
|
+
else:
|
|
68
|
+
print(f"Errors: {result.errors}")
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Full Validation (including City)
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
data = {
|
|
75
|
+
"country": "NG", # Supports codes or full names
|
|
76
|
+
"state": "LA", # Supports state codes
|
|
77
|
+
"lga": "Agege",
|
|
78
|
+
"city": "Agege" # Optional
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
result = verify(data)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Features
|
|
85
|
+
|
|
86
|
+
- **Offline-First**: Uses local JSON data for instant verification.
|
|
87
|
+
- **Data Normalization**: Automatically converts codes to full names and vice-versa.
|
|
88
|
+
- **Case Insensitive**: Accepts "LAGOS", "lagos", or "Lagos".
|
|
89
|
+
- **Dynamic Loading**: Loads only the data for the country you are verifying.
|
|
90
|
+
|
|
91
|
+
## API Reference
|
|
92
|
+
|
|
93
|
+
### `verify(user_input: dict) -> Result`
|
|
94
|
+
|
|
95
|
+
The `user_input` dictionary accepts:
|
|
96
|
+
- `country` (Required): Full name or ISO code.
|
|
97
|
+
- `state` (Required): Full name or code.
|
|
98
|
+
- `lga` (Required): Local Government Area name.
|
|
99
|
+
- `city` (Optional): City name.
|
|
100
|
+
|
|
101
|
+
Returns a `Result` object with:
|
|
102
|
+
- `valid`: Boolean.
|
|
103
|
+
- `errors`: List of strings (empty if valid).
|
|
104
|
+
- `normalized`: Dictionary of standard location names and codes.
|
|
105
|
+
|
|
106
|
+
## Contributing
|
|
107
|
+
|
|
108
|
+
Data is stored in `afrogeo/data/`. To add a new country, create a `<country_code>.json` file following the existing structure.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
afrogeo/__init__.py,sha256=fGY3zLOCrx7kJvgBSoXMSAuGXeuSMDqycEvGLTgO65Y,581
|
|
2
|
+
afrogeo/result.py,sha256=YYH_j1C3aH2hmcbIuAlQTM3ZWUIY8bigla-vLnCq5Ho,1207
|
|
3
|
+
afrogeo/verify.py,sha256=dU-Ns7mwpFGyxUr3lkRj7iAoOycomGFiI58ir36e9Gs,4790
|
|
4
|
+
afrogeo/data/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
afrogeo-0.1.0.dist-info/licenses/LICENSE,sha256=20RPtG0rhQ1nPS_d7o7HSu44_iWiyqrDZw6CEHTiHy0,1066
|
|
6
|
+
afrogeo-0.1.0.dist-info/METADATA,sha256=jhqcr_-PiL5TAaxcAA8-jnB1opkF8v1Qol2BjXQITCY,3341
|
|
7
|
+
afrogeo-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
8
|
+
afrogeo-0.1.0.dist-info/top_level.txt,sha256=Q6vlL7NjHvWikpOzYd4kmUVxrHge6PeKzxiybN3xL4U,8
|
|
9
|
+
afrogeo-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 404 Codes
|
|
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 @@
|
|
|
1
|
+
afrogeo
|