Skip to content

Commit 9ea5104

Browse files
author
jayeshmepani
committed
chore: update python support range to 3.10-3.14
1 parent e70b617 commit 9ea5104

14 files changed

Lines changed: 474 additions & 134 deletions

File tree

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ jobs:
1212
strategy:
1313
matrix:
1414
os: [ubuntu-latest, macos-latest, windows-latest]
15-
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
15+
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
1616

1717
steps:
1818
- uses: actions/checkout@v4

.github/workflows/quality.yml

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
name: Quality Checks
2+
3+
on:
4+
push:
5+
branches: [ "main" ]
6+
pull_request:
7+
branches: [ "main" ]
8+
9+
jobs:
10+
quality:
11+
name: Lint, Type Check and Test
12+
runs-on: ubuntu-latest
13+
14+
steps:
15+
- uses: actions/checkout@v4
16+
17+
- name: Set up Python
18+
uses: actions/setup-python@v5
19+
with:
20+
python-version: "3.10"
21+
cache: 'pip'
22+
23+
- name: Install dependencies
24+
run: |
25+
python -m pip install --upgrade pip
26+
pip install -e ".[dev]"
27+
28+
- name: Ruff Check (Lint)
29+
run: ruff check postalkit/ tests/
30+
31+
- name: Ruff Format (Style)
32+
run: ruff format --check postalkit/ tests/
33+
34+
- name: Pyright (Type Check)
35+
run: |
36+
pip install pyright
37+
pyright postalkit/
38+
39+
- name: Pytest (Unit Tests)
40+
run: pytest tests/

.pre-commit-config.yaml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
repos:
2+
- repo: https://github.com/astral-sh/ruff-pre-commit
3+
rev: v0.9.0
4+
hooks:
5+
- id: ruff # Lint
6+
args: [--fix, --exit-non-zero-on-fix]
7+
- id: ruff-format # Format
8+
- repo: local
9+
hooks:
10+
- id: pyright
11+
name: pyright
12+
entry: npx pyright
13+
language: node
14+
types: [python]
15+
pass_filenames: false
16+
additional_dependencies: ['pyright']

postalkit/core/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1+
from ..exceptions import DependencyMissingError, InitializationError, PostalKitError
12
from .ffi import initialize
2-
from ..exceptions import PostalKitError, InitializationError, DependencyMissingError
33

4-
__all__ = ["initialize", "PostalKitError", "InitializationError", "DependencyMissingError"]
4+
__all__ = ["DependencyMissingError", "InitializationError", "PostalKitError", "initialize"]

postalkit/core/ffi.py

Lines changed: 278 additions & 75 deletions
Large diffs are not rendered by default.

postalkit/data/checksum.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,26 @@
11
import hashlib
22
from pathlib import Path
33

4+
45
def verify_checksum(file_path: Path, expected_hash: str) -> bool:
56
"""
67
Verifies the SHA256 checksum of a file against an expected hash.
7-
8+
89
Args:
910
file_path (Path): Path to the file.
1011
expected_hash (str): The expected SHA256 hash.
11-
12+
1213
Returns:
1314
bool: True if the checksum matches, False otherwise.
1415
"""
1516
if not file_path.exists():
1617
return False
17-
18+
1819
sha256_hash = hashlib.sha256()
19-
20+
2021
with open(file_path, "rb") as f:
2122
# Read and update hash in chunks to prevent memory overload for large files
2223
for byte_block in iter(lambda: f.read(4096), b""):
2324
sha256_hash.update(byte_block)
24-
25+
2526
return sha256_hash.hexdigest() == expected_hash

postalkit/data/manager.py

Lines changed: 32 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,81 +1,97 @@
11
import os
22
from pathlib import Path
3-
from ..runtime.paths import get_binary_dir, get_model_dir
4-
from ..runtime.platform import get_platform_identifier, get_library_name
5-
from ..runtime.downloader import download_file
3+
64
from ..exceptions import DependencyMissingError
5+
from ..runtime.downloader import download_file
6+
from ..runtime.paths import get_binary_dir, get_model_dir
7+
from ..runtime.platform import get_library_name, get_platform_identifier
78
from .checksum import verify_checksum
89

910
POSTALKIT_VERSION = "v1.0.5"
10-
BINARIES_BASE_URL = os.environ.get("POSTALKIT_BINARIES_URL", f"https://github.com/jayeshmepani/libpostal-ffi-python/releases/download/{POSTALKIT_VERSION}")
11-
MODEL_DATA_URL = os.environ.get("POSTALKIT_MODEL_URL", "https://s3.amazonaws.com/libpostal/data/libpostal_data.tar.gz")
11+
BINARIES_BASE_URL = os.environ.get(
12+
"POSTALKIT_BINARIES_URL",
13+
f"https://github.com/jayeshmepani/libpostal-ffi-python/releases/download/{POSTALKIT_VERSION}",
14+
)
15+
MODEL_DATA_URL = os.environ.get(
16+
"POSTALKIT_MODEL_URL", "https://s3.amazonaws.com/libpostal/data/libpostal_data.tar.gz"
17+
)
18+
1219

1320
def _download_and_verify(url: str, tar_path: Path, desc: str):
1421
"""Downloads a file and its .sha256 checksum file, then verifies and extracts."""
1522
# 1. Download the archive
1623
download_file(url, tar_path, extract=False, desc=desc)
17-
24+
1825
# 2. Download the checksum
1926
checksum_url = f"{url}.sha256"
2027
checksum_path = tar_path.with_suffix(".tar.gz.sha256")
2128
try:
2229
download_file(checksum_url, checksum_path, extract=False, desc=f"{desc} Checksum")
23-
with open(checksum_path, "r") as f:
30+
with open(checksum_path) as f:
2431
# typical format: "hash filename"
2532
expected_hash = f.read().strip().split()[0]
26-
33+
2734
if not verify_checksum(tar_path, expected_hash):
2835
os.remove(tar_path)
29-
raise DependencyMissingError(f"Checksum verification failed for {tar_path.name}")
36+
raise DependencyMissingError from None(
37+
f"Checksum verification failed for {tar_path.name}"
38+
)
3039
except Exception as e:
3140
# If the checksum file doesn't exist remotely or fails to download,
3241
# we strictly fail to prevent compromised or corrupt binaries in production.
3342
if tar_path.exists():
3443
os.remove(tar_path)
3544
if checksum_path.exists():
3645
os.remove(checksum_path)
37-
raise DependencyMissingError(f"Failed to fetch or verify checksum for {tar_path.name}: {e}")
46+
raise DependencyMissingError from None(
47+
f"Failed to fetch or verify checksum for {tar_path.name}: {e}"
48+
)
3849

3950
# 3. Extract after verification
4051
from ..runtime.downloader import _extract_tar_gz
52+
4153
_extract_tar_gz(tar_path, tar_path.parent)
4254

55+
4356
def ensure_models() -> Path:
4457
"""Ensures libpostal data models are downloaded and returns their path."""
4558
model_dir = get_model_dir()
4659
marker_file = model_dir / "data_version"
47-
60+
4861
if not marker_file.exists():
4962
tar_path = model_dir / "libpostal_data.tar.gz"
5063
print("PostalKit models not found. Downloading (~2GB)...")
5164
_download_and_verify(MODEL_DATA_URL, tar_path, "Model Data")
5265
marker_file.write_text("1")
53-
66+
5467
return model_dir
5568

69+
5670
def ensure_binary() -> Path:
5771
"""Ensures the correct shared library for the platform is downloaded."""
5872
from ..runtime.paths import get_bundled_binary_dir
73+
5974
bin_dir = get_binary_dir()
6075
lib_name = get_library_name()
6176
platform_id = get_platform_identifier()
62-
77+
6378
# 1. Check bundled library
6479
bundled_lib_path = get_bundled_binary_dir() / lib_name
6580
if bundled_lib_path.exists():
6681
return bundled_lib_path
67-
82+
6883
# 2. Check auto-downloaded library
6984
lib_path = bin_dir / lib_name
70-
85+
7186
if not lib_path.exists():
7287
download_url = f"{BINARIES_BASE_URL}/libpostal-{platform_id}.tar.gz"
7388
tar_path = bin_dir / f"libpostal-{platform_id}.tar.gz"
7489
print(f"PostalKit binary not found. Downloading for {platform_id}...")
7590
_download_and_verify(download_url, tar_path, "Binary")
76-
91+
7792
return lib_path
7893

94+
7995
def ensure_all_assets():
8096
"""Download models and binaries if missing."""
8197
ensure_models()

postalkit/exceptions.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,22 @@
11
class PostalKitError(Exception):
22
"""Base exception for all PostalKit errors."""
3+
34
pass
45

6+
57
class InitializationError(PostalKitError):
68
"""Raised when libpostal fails to initialize (e.g., memory issues, bad data dir)."""
9+
710
pass
811

12+
913
class DependencyMissingError(PostalKitError):
1014
"""Raised when required binaries or data models cannot be downloaded or found."""
15+
1116
pass
1217

18+
1319
class ParsingError(PostalKitError):
1420
"""Raised when the C library encounters an error parsing an address."""
21+
1522
pass

postalkit/runtime/downloader.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,26 @@
11
import os
22
import tarfile
3-
import urllib.request
43
from pathlib import Path
4+
55
import httpx
66
from tqdm import tqdm
7+
78
from ..exceptions import DependencyMissingError
89

10+
911
def download_file(url: str, dest_path: Path, extract: bool = False, desc: str = "Downloading"):
1012
"""Downloads a file from a URL to a destination path, optionally extracting it."""
1113
dest_path.parent.mkdir(parents=True, exist_ok=True)
12-
14+
1315
# Simple check if it's already there (for the archive itself, or skipped if extraction is managed externally)
1416
if dest_path.exists():
1517
return
16-
18+
1719
try:
1820
with httpx.stream("GET", url, follow_redirects=True) as response:
1921
response.raise_for_status()
2022
total_size = int(response.headers.get("content-length", 0))
21-
23+
2224
with open(dest_path, "wb") as f, tqdm(
2325
desc=desc,
2426
total=total_size,
@@ -32,16 +34,17 @@ def download_file(url: str, dest_path: Path, extract: bool = False, desc: str =
3234
except Exception as e:
3335
if dest_path.exists():
3436
os.remove(dest_path)
35-
raise DependencyMissingError(f"Failed to download {url}: {e}")
37+
raise DependencyMissingError from None(f"Failed to download {url}: {e}")
3638

3739
if extract and str(dest_path).endswith(".tar.gz"):
3840
_extract_tar_gz(dest_path, dest_path.parent)
3941

42+
4043
def _extract_tar_gz(tar_path: Path, extract_path: Path):
4144
print(f"Extracting {tar_path.name}...")
4245
try:
4346
with tarfile.open(tar_path, "r:gz") as tar:
4447
tar.extractall(path=extract_path)
4548
os.remove(tar_path)
4649
except Exception as e:
47-
raise DependencyMissingError(f"Failed to extract {tar_path}: {e}")
50+
raise DependencyMissingError from None(f"Failed to extract {tar_path}: {e}")

postalkit/runtime/loader.py

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,40 @@
11
import ctypes
2+
3+
from ..exceptions import DependencyMissingError
24
from ..runtime.paths import get_binary_dir, get_bundled_binary_dir
35
from ..runtime.platform import get_library_name
4-
from ..exceptions import DependencyMissingError
6+
57

68
def load_libpostal() -> ctypes.CDLL:
79
"""Loads the libpostal shared library into ctypes."""
810
lib_name = get_library_name()
9-
11+
1012
# 1. Check bundled library first
1113
bundled_lib_path = get_bundled_binary_dir() / lib_name
1214
if bundled_lib_path.exists():
1315
try:
1416
return ctypes.cdll.LoadLibrary(str(bundled_lib_path))
1517
except OSError as e:
16-
raise DependencyMissingError(f"Failed to load bundled library {bundled_lib_path}: {e}")
17-
18+
raise DependencyMissingError from None(
19+
f"Failed to load bundled library {bundled_lib_path}: {e}"
20+
)
21+
1822
# 2. Check auto-downloaded library
1923
bin_dir = get_binary_dir()
2024
lib_path = bin_dir / lib_name
21-
25+
2226
if not lib_path.exists():
2327
# Maybe it's installed system-wide?
2428
try:
2529
return ctypes.cdll.LoadLibrary(lib_name)
2630
except OSError:
2731
pass
28-
raise DependencyMissingError(f"Could not find {lib_name} in {bin_dir}, bundled {bundled_lib_path}, or system paths. Run initialization first.")
29-
32+
raise DependencyMissingError from None(
33+
f"Could not find {lib_name} in {bin_dir}, bundled {bundled_lib_path}, or system paths. Run initialization first."
34+
)
35+
3036
try:
3137
# Load the library using ctypes
3238
return ctypes.cdll.LoadLibrary(str(lib_path))
3339
except OSError as e:
34-
raise DependencyMissingError(f"Failed to load shared library {lib_path}: {e}")
40+
raise DependencyMissingError from None(f"Failed to load shared library {lib_path}: {e}")

0 commit comments

Comments
 (0)