Compare commits
No commits in common. "main" and "v0.2.1" have entirely different histories.
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,5 +1,5 @@
|
|||||||
# claude
|
# claude
|
||||||
.claude/
|
CLAUDE.md
|
||||||
|
|
||||||
# python
|
# python
|
||||||
__pycache__/
|
__pycache__/
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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 = []
|
||||||
|
|
||||||
|
|||||||
@ -63,4 +63,4 @@ __all__ = [
|
|||||||
"aretry",
|
"aretry",
|
||||||
]
|
]
|
||||||
|
|
||||||
__version__ = "0.2.3"
|
__version__ = "0.2.1"
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user