diff --git a/README.md b/README.md index 5badded..6176c1f 100644 --- a/README.md +++ b/README.md @@ -12,9 +12,9 @@ Small sync helpers shared across projects. Base is stdlib only — **no dependen ## Install ``` -commons @ git+ssh://git@git.rethinkstudios.io/rethink-public/commons.git@v0.2.1 +commons @ git+ssh://git@git.rethinkstudios.io/rethink-public/commons.git@v0.2.2 # async address/geo lookups (fetch_ip / ip_location / fetch_location) need the extra: -commons[addr] @ git+ssh://git@git.rethinkstudios.io/rethink-public/commons.git@v0.2.1 +commons[addr] @ git+ssh://git@git.rethinkstudios.io/rethink-public/commons.git@v0.2.2 ``` The base install pulls **nothing** (stdlib). Only `commons[addr]` adds `aiohttp`, and diff --git a/ledger.md b/ledger.md new file mode 100644 index 0000000..e041062 --- /dev/null +++ b/ledger.md @@ -0,0 +1,28 @@ +# commons — ledger + +## v0.2.2 + +- **fidelity fix (HIGH):** `addr.geo.fetch_location` state slug now uses an underscore + separator and an NFKD ascii-fold, matching the original `utils.py` contract the live + proxy `region-` token depends on (`New York` → `new_york`, `Québec` → `quebec`). The + rewrite had produced a hyphen slug with no fold (`new-york`), which silently routed + multi-word states to an unrecognized region. Added `_state_slug`; routed + `_parse_reverse` through it. + +## v0.2.1 + +- floor `retry`/`aretry` `attempts` at 1 (was: `attempts=0` silently returned None). +- retry log uses total `attempts` as the denominator (was `attempts-1` → `retry 1/2`). +- geo lookups forward the per-request `timeout` to `session.get` on the injected-session + path (was honored only on the owned-session path). + +## v0.2.0 + +- added the `retry` module: `retry` (sync) + `aretry` (async), call-form or decorator, + exponential backoff with optional full jitter, `on=` narrowing, `give_up` early-stop, + fail-loud re-raise of the last exception. + +## v0.1.0 + +- initial: timing (unix deltas + tz Clock), paths (dotted deep_get/set), masking + (card/cvv/token display), addr (ip utils + geo behind the `[addr]` extra). diff --git a/pyproject.toml b/pyproject.toml index 801eab1..3fde378 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "commons" -version = "0.2.1" +version = "0.2.2" description = "small stdlib-only sync helpers: time/timezone deltas, dotted-path dict access, display masking, ip/address tooling, and retry/backoff" requires-python = ">=3.10" dependencies = [] diff --git a/src/commons/__init__.py b/src/commons/__init__.py index 22ef211..fc43732 100644 --- a/src/commons/__init__.py +++ b/src/commons/__init__.py @@ -63,4 +63,4 @@ __all__ = [ "aretry", ] -__version__ = "0.2.1" +__version__ = "0.2.2" diff --git a/src/commons/addr/geo.py b/src/commons/addr/geo.py index 2619b12..810d67b 100644 --- a/src/commons/addr/geo.py +++ b/src/commons/addr/geo.py @@ -12,6 +12,7 @@ security: the only secret is geo.ipify's `api_key`, which is a REQUIRED keyword `ip_location` — the caller injects it. nothing is hardcoded here. """ import logging +import unicodedata from typing import Optional from urllib.parse import urlencode @@ -55,6 +56,12 @@ def _parse_ipify(data: dict) -> Optional[str]: return ip or None +def _state_slug(state: str) -> str: + """lowercase ascii-folded state slug with underscores, matching the live proxy region- contract""" + folded = unicodedata.normalize("NFKD", state).encode("ascii", "ignore").decode("ascii") + return folded.lower().replace(" ", "_") + + def _parse_reverse(data: dict) -> Optional[dict]: """parse a nominatim reverse response into {country: iso2 lower, state: slug|None}""" address = data.get("address") @@ -66,7 +73,7 @@ def _parse_reverse(data: dict) -> Optional[dict]: state = address.get("state") return { "country": str(country).lower(), - "state": state.lower().replace(" ", "-") if state else None, + "state": _state_slug(state) if state else None, }