From 449f7905715676cedbdf7832813a64ca4ba5dc3f Mon Sep 17 00:00:00 2001 From: disqualifier Date: Mon, 29 Jun 2026 17:57:54 -0400 Subject: [PATCH] fix: guard geo parse helpers against non-dict JSON; de-dup timeout build (v0.2.3) - _parse_ipify/_parse_reverse return None on a truthy non-dict body instead of raising AttributeError, honoring the documented 'None on any parse failure' contract (L9) - build the geo request ClientTimeout once instead of twice (nit) - drop the stale 'live proxy region- contract' wording from _state_slug's docstring (nit). Signed-off-by: disqualifier --- README.md | 4 ++-- pyproject.toml | 2 +- src/commons/__init__.py | 2 +- src/commons/addr/geo.py | 10 +++++++--- 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 6176c1f..ce3db0c 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.2 +commons @ git+ssh://git@git.rethinkstudios.io/rethink-public/commons.git@v0.2.3 # 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.2 +commons[addr] @ git+ssh://git@git.rethinkstudios.io/rethink-public/commons.git@v0.2.3 ``` The base install pulls **nothing** (stdlib). Only `commons[addr]` adds `aiohttp`, and diff --git a/pyproject.toml b/pyproject.toml index 3fde378..cc3de55 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "commons" -version = "0.2.2" +version = "0.2.3" 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 fc43732..d4c8e05 100644 --- a/src/commons/__init__.py +++ b/src/commons/__init__.py @@ -63,4 +63,4 @@ __all__ = [ "aretry", ] -__version__ = "0.2.2" +__version__ = "0.2.3" diff --git a/src/commons/addr/geo.py b/src/commons/addr/geo.py index 810d67b..4d11443 100644 --- a/src/commons/addr/geo.py +++ b/src/commons/addr/geo.py @@ -52,18 +52,22 @@ def _reverse_url(lat: float, lon: float) -> str: def _parse_ipify(data: dict) -> Optional[str]: """pull the ip string out of an ipify response""" + if not isinstance(data, dict): + return None ip = data.get("ip") return ip or None def _state_slug(state: str) -> str: - """lowercase ascii-folded state slug with underscores, matching the live proxy region- contract""" + """lowercase ascii-folded state slug with underscores (e.g. 'New York' -> 'new_york')""" 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}""" + if not isinstance(data, dict): + return None address = data.get("address") if not isinstance(address, dict): return None @@ -84,10 +88,10 @@ async def _get_json( if not _HAVE_AIOHTTP: raise RuntimeError(_MISSING) owns = session is None + request_timeout = aiohttp.ClientTimeout(total=timeout) if owns: - session = aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=timeout)) + session = aiohttp.ClientSession(timeout=request_timeout) try: - request_timeout = aiohttp.ClientTimeout(total=timeout) async with session.get(url, headers=headers, timeout=request_timeout) as resp: if resp.status != 200: log.warning("address lookup %s -> %s", url, resp.status)