Skip to content

Commit 21ab647

Browse files
committed
feat: add initial implementation of lakhua library with README, setup, and tests
1 parent 501e3c8 commit 21ab647

File tree

19 files changed

+77624
-0
lines changed

19 files changed

+77624
-0
lines changed

.github/workflows/pypi-publish.yml

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
name: Publish Python Package to PyPI
2+
3+
on:
4+
release:
5+
types: [published]
6+
workflow_dispatch:
7+
8+
concurrency:
9+
group: pypi-publish
10+
cancel-in-progress: false
11+
12+
permissions:
13+
contents: read
14+
id-token: write
15+
16+
jobs:
17+
build:
18+
runs-on: ubuntu-latest
19+
defaults:
20+
run:
21+
working-directory: libs/python
22+
23+
steps:
24+
- name: Checkout repository
25+
uses: actions/checkout@v4
26+
27+
- name: Setup Python
28+
uses: actions/setup-python@v5
29+
with:
30+
python-version: "3.12"
31+
32+
- name: Install dependencies
33+
run: |
34+
python -m pip install --upgrade pip
35+
pip install build twine pytest ruff mypy
36+
37+
- name: Install package
38+
run: pip install -e ".[dev]"
39+
40+
- name: Run linting
41+
run: ruff check .
42+
43+
- name: Run type checking
44+
run: mypy lakhua --ignore-missing-imports
45+
46+
- name: Run tests
47+
run: pytest
48+
49+
- name: Build package
50+
run: python -m build
51+
52+
- name: Check distribution metadata
53+
run: python -m twine check dist/*
54+
55+
- name: Upload distribution artifacts
56+
uses: actions/upload-artifact@v4
57+
with:
58+
name: python-dist
59+
path: libs/python/dist/
60+
61+
publish:
62+
runs-on: ubuntu-latest
63+
needs: build
64+
environment:
65+
name: pypi
66+
url: https://pypi.org/p/lakhua
67+
68+
permissions:
69+
id-token: write
70+
71+
steps:
72+
- name: Download distribution artifacts
73+
uses: actions/download-artifact@v4
74+
with:
75+
name: python-dist
76+
path: dist/
77+
78+
- name: Publish to PyPI
79+
uses: pypa/gh-action-pypi-publish@release/v1
80+
with:
81+
packages-dir: dist/
82+

libs/python/.gitignore

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Python
2+
__pycache__/
3+
*.py[cod]
4+
*.so
5+
*.egg
6+
*.egg-info/
7+
dist/
8+
build/
9+
*.whl
10+
.pytest_cache/
11+
.coverage
12+
htmlcov/
13+
.mypy_cache/
14+
.ruff_cache/
15+
16+
# Virtual environments
17+
venv/
18+
env/
19+
ENV/
20+
21+
# IDEs
22+
.vscode/
23+
.idea/
24+
*.swp
25+
*.swo
26+
27+
# OS
28+
.DS_Store
29+
Thumbs.db
30+

libs/python/.ruff.toml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
line-length = 100
2+
3+
[lint]
4+
select = ["E", "F", "I", "N", "W", "UP", "B", "C4", "PIE", "PT", "RET", "SIM"]
5+
ignore = []
6+
7+
[lint.isort]
8+
known-first-party = ["lakhua"]
9+
10+
[format]
11+
indent-style = "space"
12+
quote-style = "double"
13+

libs/python/MANIFEST.in

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
include README.md
2+
include LICENSE
3+
recursive-include lakhua/data *.json
4+
include lakhua/py.typed
5+

libs/python/README.md

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
# lakhua (Python)
2+
3+
Offline reverse geocoding for India, optimized for fast in-memory lookups using H3 indexes.
4+
5+
## Features
6+
7+
- **Offline-first**: No API keys, no network dependency
8+
- **In-memory lookup**: Singleton loader for efficient data management
9+
- **Parent-resolution fallback**: Automatic fallback from resolution 5 → 4
10+
- **Type-safe**: Full type hints and dataclasses
11+
- **Modern Python**: Supports Python 3.8+
12+
- **Fast**: In-memory map-based lookups with minimal overhead
13+
14+
## Installation
15+
16+
```bash
17+
pip install lakhua
18+
```
19+
20+
## Quick Start
21+
22+
```python
23+
from lakhua import geocode
24+
25+
location = geocode(28.6139, 77.209)
26+
27+
if location:
28+
print(location.city, location.state)
29+
```
30+
31+
## API Reference
32+
33+
### Recommended top-level APIs
34+
35+
- `geocode(lat, lon, options=None) -> GeocodeResult | None`
36+
- `geocode_h3(h3_index, options=None) -> GeocodeResult | None`
37+
38+
These helpers use the default singleton geocoder internally.
39+
40+
### Advanced class APIs
41+
42+
- `ReverseGeocoder.get_instance()`
43+
- `DataLoader.get_instance()`
44+
45+
Use these only when you need explicit control in advanced runtime or testing scenarios.
46+
47+
### `GeocodeOptions`
48+
49+
```python
50+
@dataclass
51+
class GeocodeOptions:
52+
resolution: int = 5 # H3 resolution for geocode(lat, lon)
53+
fallback: bool = True # Enable parent resolution fallback
54+
debug: bool = False # Print timing logs
55+
```
56+
57+
### `GeocodeResult`
58+
59+
```python
60+
@dataclass
61+
class GeocodeResult:
62+
city: str
63+
state: str
64+
district: str | None
65+
pincode: str | None
66+
matched_h3: str
67+
matched_resolution: int
68+
```
69+
70+
Returns `None` for invalid inputs or when no match is found.
71+
72+
## Examples
73+
74+
### Coordinate lookup
75+
76+
```python
77+
from lakhua import geocode
78+
79+
result = geocode(12.9716, 77.5946)
80+
print(result)
81+
```
82+
83+
### Direct H3 lookup
84+
85+
```python
86+
from lakhua import geocode_h3
87+
88+
result = geocode_h3("8560145bfffffff")
89+
print(result)
90+
```
91+
92+
### Debug mode
93+
94+
```python
95+
from lakhua import geocode, GeocodeOptions
96+
97+
result = geocode(19.076, 72.8777, GeocodeOptions(debug=True))
98+
print(result)
99+
```
100+
101+
### Custom resolution
102+
103+
```python
104+
from lakhua import geocode, GeocodeOptions
105+
106+
result = geocode(
107+
28.6139, 77.2090,
108+
GeocodeOptions(resolution=4, fallback=False)
109+
)
110+
```
111+
112+
## Performance Notes
113+
114+
- Data files are loaded once per process into memory
115+
- Lookups are map-based in-memory operations
116+
- Fallback adds up to two additional parent checks (5 → 4) when enabled
117+
- Average lookup time: < 1ms (in-memory)
118+
119+
## Project Layout
120+
121+
```text
122+
lakhua/
123+
core/
124+
__init__.py
125+
constants.py
126+
data_loader.py
127+
geocoder.py
128+
types.py
129+
__init__.py
130+
data/
131+
reverse_geo_4.json
132+
reverse_geo_5.json
133+
```
134+
135+
## Development
136+
137+
### Setup
138+
139+
```bash
140+
pip install -e ".[dev]"
141+
```
142+
143+
### Run tests
144+
145+
```bash
146+
pytest
147+
```
148+
149+
### Linting and formatting
150+
151+
```bash
152+
ruff check .
153+
ruff format .
154+
```
155+
156+
### Type checking
157+
158+
```bash
159+
mypy lakhua
160+
```
161+
162+
## License
163+
164+
MIT
165+

libs/python/lakhua/__init__.py

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
"""
2+
lakhua: Fast, offline reverse geocoding for India.
3+
4+
This library provides in-memory reverse geocoding using H3 spatial indexing.
5+
"""
6+
7+
from lakhua.core import (
8+
DATA_DIR_NAME,
9+
DATA_FILE_PREFIX,
10+
DEFAULT_RESOLUTION,
11+
MAX_RESOLUTION,
12+
MIN_RESOLUTION,
13+
SUPPORTED_RESOLUTIONS,
14+
DataLoader,
15+
ReverseGeocoder,
16+
default_data_loader,
17+
default_geocoder,
18+
)
19+
from lakhua.types import GeocodeOptions, GeocodeResult, LocationDetails
20+
21+
__version__ = "0.1.0"
22+
23+
__all__ = [
24+
"__version__",
25+
"DATA_DIR_NAME",
26+
"DATA_FILE_PREFIX",
27+
"DEFAULT_RESOLUTION",
28+
"MAX_RESOLUTION",
29+
"MIN_RESOLUTION",
30+
"SUPPORTED_RESOLUTIONS",
31+
"DataLoader",
32+
"ReverseGeocoder",
33+
"default_data_loader",
34+
"default_geocoder",
35+
"GeocodeOptions",
36+
"GeocodeResult",
37+
"LocationDetails",
38+
"geocode",
39+
"geocode_h3",
40+
]
41+
42+
43+
def geocode(
44+
lat: float,
45+
lon: float,
46+
options: GeocodeOptions | None = None,
47+
) -> GeocodeResult | None:
48+
"""
49+
Reverse geocodes latitude/longitude into India location metadata.
50+
51+
Uses the library's internal singleton geocoder, so you can call this directly
52+
without creating any class instance.
53+
54+
Args:
55+
lat: Latitude in decimal degrees.
56+
lon: Longitude in decimal degrees.
57+
options: Lookup options such as fallback and debug logging.
58+
59+
Returns:
60+
Matched location details, or None when input is invalid / no match exists.
61+
62+
Example:
63+
>>> from lakhua import geocode
64+
>>> result = geocode(28.6139, 77.2090)
65+
>>> print(result.city, result.state)
66+
"""
67+
return default_geocoder.geocode(lat, lon, options)
68+
69+
70+
def geocode_h3(
71+
h3_index: str,
72+
options: GeocodeOptions | None = None,
73+
) -> GeocodeResult | None:
74+
"""
75+
Reverse geocodes an H3 cell index directly.
76+
77+
Uses the library's internal singleton geocoder. When fallback is enabled
78+
(default), parent resolutions are checked until the minimum supported resolution.
79+
80+
Args:
81+
h3_index: H3 cell index string.
82+
options: Lookup options such as fallback and debug logging.
83+
84+
Returns:
85+
Matched location details, or None when input is invalid / no match exists.
86+
87+
Example:
88+
>>> from lakhua import geocode_h3
89+
>>> result = geocode_h3("8560145bfffffff")
90+
>>> print(result.city if result else "No match")
91+
"""
92+
return default_geocoder.geocode_h3(h3_index, options)
93+

0 commit comments

Comments
 (0)