Compare commits

..

No commits in common. "main" and "v0.2.1" have entirely different histories.
main ... v0.2.1

5 changed files with 9 additions and 26 deletions

2
.gitignore vendored
View File

@ -1,5 +1,5 @@
# claude # claude
.claude/ CLAUDE.md
# python # python
__pycache__/ __pycache__/

View File

@ -12,16 +12,14 @@ Small sync helpers shared across projects. Base is stdlib only — **no dependen
## Install ## Install
``` ```
commons @ git+ssh://git@git.rethinkstudios.io/rethink-public/commons.git@v0.2.3 commons @ git+ssh://git@git.rethinkstudios.io/rethink-public/commons.git@v0.2.1
# async address/geo lookups (fetch_ip / ip_location / fetch_location) need the extra: # 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.3 commons[addr] @ git+ssh://git@git.rethinkstudios.io/rethink-public/commons.git@v0.2.1
``` ```
The base install pulls **nothing** (stdlib). Only `commons[addr]` adds `aiohttp`, and The base install pulls **nothing** (stdlib). Only `commons[addr]` adds `aiohttp`, and
only for the geo lookups — the pure `commons.addr.ip` utilities ship in base. only for the geo lookups — the pure `commons.addr.ip` utilities ship in base.
Drop the `@v0.2.3` suffix from the line above to install the latest unpinned.
## timing ## timing
Unix ints stay the storable value; datetimes are produced on demand in whatever Unix ints stay the storable value; datetimes are produced on demand in whatever

View File

@ -4,8 +4,8 @@ build-backend = "hatchling.build"
[project] [project]
name = "commons" name = "commons"
version = "0.2.3" version = "0.2.1"
description = "small stdlib-based sync helpers: time/timezone deltas, dotted-path dict access, display masking, ip/address tooling, and retry/backoff" 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" requires-python = ">=3.10"
dependencies = [] dependencies = []

View File

@ -63,4 +63,4 @@ __all__ = [
"aretry", "aretry",
] ]
__version__ = "0.2.3" __version__ = "0.2.1"

View File

@ -12,7 +12,6 @@ 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. `ip_location` the caller injects it. nothing is hardcoded here.
""" """
import logging import logging
import unicodedata
from typing import Optional from typing import Optional
from urllib.parse import urlencode from urllib.parse import urlencode
@ -52,26 +51,12 @@ def _reverse_url(lat: float, lon: float) -> str:
def _parse_ipify(data: dict) -> Optional[str]: def _parse_ipify(data: dict) -> Optional[str]:
"""pull the ip string out of an ipify response""" """pull the ip string out of an ipify response"""
if not isinstance(data, dict):
return None
ip = data.get("ip") ip = data.get("ip")
return ip or None return ip or None
def _state_slug(state) -> str:
"""lowercase ascii-folded state slug with underscores (e.g. 'New York' -> 'new_york')
coerces to str first so a malformed non-string `state` doesn't raise TypeError out
of unicodedata.normalize and break the 'None on any parse failure' contract.
"""
folded = unicodedata.normalize("NFKD", str(state)).encode("ascii", "ignore").decode("ascii")
return folded.lower().replace(" ", "_")
def _parse_reverse(data: dict) -> Optional[dict]: def _parse_reverse(data: dict) -> Optional[dict]:
"""parse a nominatim reverse response into {country: iso2 lower, state: slug|None}""" """parse a nominatim reverse response into {country: iso2 lower, state: slug|None}"""
if not isinstance(data, dict):
return None
address = data.get("address") address = data.get("address")
if not isinstance(address, dict): if not isinstance(address, dict):
return None return None
@ -81,7 +66,7 @@ def _parse_reverse(data: dict) -> Optional[dict]:
state = address.get("state") state = address.get("state")
return { return {
"country": str(country).lower(), "country": str(country).lower(),
"state": _state_slug(state) if state else None, "state": state.lower().replace(" ", "-") if state else None,
} }
@ -92,10 +77,10 @@ async def _get_json(
if not _HAVE_AIOHTTP: if not _HAVE_AIOHTTP:
raise RuntimeError(_MISSING) raise RuntimeError(_MISSING)
owns = session is None owns = session is None
request_timeout = aiohttp.ClientTimeout(total=timeout)
if owns: if owns:
session = aiohttp.ClientSession(timeout=request_timeout) session = aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=timeout))
try: try:
request_timeout = aiohttp.ClientTimeout(total=timeout)
async with session.get(url, headers=headers, timeout=request_timeout) as resp: async with session.get(url, headers=headers, timeout=request_timeout) as resp:
if resp.status != 200: if resp.status != 200:
log.warning("address lookup %s -> %s", url, resp.status) log.warning("address lookup %s -> %s", url, resp.status)