docs: compress prose/module docstrings, em-dash->hyphen (de-bloat wave 1)
Signed-off-by: disqualifier <dev@disqualifier.me>
This commit is contained in:
@@ -12,15 +12,15 @@ 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.3.2
|
commons @ git+ssh://git@git.rethinkstudios.io/rethink-public/commons.git@v0.3.3
|
||||||
# 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.3.2
|
commons[addr] @ git+ssh://git@git.rethinkstudios.io/rethink-public/commons.git@v0.3.3
|
||||||
```
|
```
|
||||||
|
|
||||||
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.3.2` suffix from the line above to install the latest unpinned.
|
Drop the `@v0.3.3` suffix from the line above to install the latest unpinned.
|
||||||
|
|
||||||
## timing
|
## timing
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "commons"
|
name = "commons"
|
||||||
version = "0.3.2"
|
version = "0.3.3"
|
||||||
description = "small stdlib-based sync helpers: time/timezone deltas, dotted-path dict access, display masking, ip/address tooling, and retry/backoff"
|
description = "small stdlib-based 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 = []
|
||||||
|
|||||||
+3
-18
@@ -1,22 +1,7 @@
|
|||||||
"""commons — small sync helpers shared across projects.
|
"""commons - small sync helpers shared across projects: timing, paths, masking, addr, retry.
|
||||||
|
|
||||||
timing: unix-timestamp deltas + timezone-aware datetime conversions, bare
|
|
||||||
functions and a configurable Clock.
|
|
||||||
paths: nested dict/list access by dotted path (deep_get / deep_set); deep_set
|
|
||||||
mirrors deep_get's list indexing, so a get/set round-trip on the same
|
|
||||||
path never destroys a list.
|
|
||||||
masking: display masking for cards / cvv / tokens, plus url/proxy credential
|
|
||||||
redaction for logging (cosmetic, not a security control).
|
|
||||||
addr: ip/address tooling — pure stdlib ip utils (`commons.addr.ip`) in base,
|
|
||||||
async geo lookups (`commons.addr.geo`) behind the `commons[addr]` extra.
|
|
||||||
retry: exponential-backoff retry, sync (`retry`) and async (`aretry`), call or
|
|
||||||
decorator form; re-raises the last exception (fail loud), never swallows.
|
|
||||||
|
|
||||||
base is stdlib only, no dependencies (the addr geo lookups add aiohttp via the
|
base is stdlib only, no dependencies (the addr geo lookups add aiohttp via the
|
||||||
extra). to toggle the timing test mode for bare calls, set it on the module —
|
`commons[addr]` extra). see each submodule's docstring for its api.
|
||||||
`from commons import timing; timing.FAST_MODE = True` — or use `Clock(fast=True)`
|
|
||||||
for instance-scoped control. addr is exposed as a submodule (`from commons import
|
|
||||||
addr`); its ip helpers live under `commons.addr.ip` to keep top-level uncluttered.
|
|
||||||
"""
|
"""
|
||||||
from . import addr, masking, paths, timing
|
from . import addr, masking, paths, timing
|
||||||
from .masking import credit, cvv, mask_proxy, mask_url, phantom, provider
|
from .masking import credit, cvv, mask_proxy, mask_url, phantom, provider
|
||||||
@@ -68,4 +53,4 @@ __all__ = [
|
|||||||
"aretry",
|
"aretry",
|
||||||
]
|
]
|
||||||
|
|
||||||
__version__ = "0.3.2"
|
__version__ = "0.3.3"
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
"""addr — ip/address tooling for commons.
|
"""addr - ip/address tooling for commons.
|
||||||
|
|
||||||
two concerns:
|
`ip`: pure stdlib ipaddress utilities, ships in the base install, no dependencies.
|
||||||
- `ip` — pure stdlib ipaddress utilities (validation, membership, cidr shape,
|
`geo`: async network lookups (aiohttp), gated behind the `commons[addr]` extra;
|
||||||
conversions). ships in the base install, no dependencies.
|
importing this package is fine without it, but calling a geo function raises until
|
||||||
- `geo` — async network lookups (public ip, ip->geo, reverse geocode). needs
|
it is installed. the geo.ipify api_key is always injected by the caller, never
|
||||||
aiohttp, gated behind the `commons[addr]` extra; importing this package is fine
|
hardcoded.
|
||||||
without it, but calling a geo function raises until it is installed.
|
|
||||||
|
|
||||||
the geo.ipify api_key is always injected by the caller — nothing is hardcoded.
|
|
||||||
"""
|
"""
|
||||||
from . import geo, ip
|
from . import geo, ip
|
||||||
from .geo import fetch_ip, fetch_location, ip_location
|
from .geo import fetch_ip, fetch_location, ip_location
|
||||||
|
|||||||
+7
-15
@@ -1,15 +1,10 @@
|
|||||||
"""async ip/geo network lookups (aiohttp, gated behind the [addr] extra).
|
"""async ip/geo network lookups (aiohttp, gated behind the [addr] extra).
|
||||||
|
|
||||||
importing this module without aiohttp is fine; only calling a lookup without it
|
importing this module without aiohttp is fine; only calling a lookup without it
|
||||||
raises a clear RuntimeError. each call may reuse a caller-supplied aiohttp
|
raises a clear RuntimeError. every lookup returns None on any request/parse
|
||||||
ClientSession (`session=`) or create and close one internally.
|
failure (logged, never raised, never printed). the only secret is geo.ipify's
|
||||||
|
`api_key`, a REQUIRED keyword on `ip_location` - the caller injects it, nothing
|
||||||
url-building and json->dict parsing are pulled into pure helpers (`_*_url`,
|
is hardcoded here.
|
||||||
`_parse_*`) so they unit-test without network. every lookup returns None on any
|
|
||||||
request/parse failure (logged, never raised, never printed).
|
|
||||||
|
|
||||||
security: the only secret is geo.ipify's `api_key`, which is a REQUIRED keyword on
|
|
||||||
`ip_location` — the caller injects it. nothing is hardcoded here.
|
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
import unicodedata
|
import unicodedata
|
||||||
@@ -59,11 +54,8 @@ def _parse_ipify(data: dict) -> Optional[str]:
|
|||||||
|
|
||||||
|
|
||||||
def _state_slug(state) -> str:
|
def _state_slug(state) -> str:
|
||||||
"""lowercase ascii-folded state slug with underscores (e.g. 'New York' -> 'new_york')
|
"""lowercase ascii-folded state slug with underscores (e.g. 'New York' -> 'new_york');
|
||||||
|
coerces to str first so a non-string `state` can't TypeError out of the parse contract"""
|
||||||
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")
|
folded = unicodedata.normalize("NFKD", str(state)).encode("ascii", "ignore").decode("ascii")
|
||||||
return folded.lower().replace(" ", "_")
|
return folded.lower().replace(" ", "_")
|
||||||
|
|
||||||
@@ -121,7 +113,7 @@ async def ip_location(
|
|||||||
"""look up geo data for an ip via geo.ipify; api_key is required and injected
|
"""look up geo data for an ip via geo.ipify; api_key is required and injected
|
||||||
|
|
||||||
returns the raw geo.ipify json dict, or None on failure. raises ValueError if
|
returns the raw geo.ipify json dict, or None on failure. raises ValueError if
|
||||||
no api_key is supplied — there is no default and nothing hardcoded.
|
no api_key is supplied - there is no default and nothing hardcoded.
|
||||||
"""
|
"""
|
||||||
if not api_key:
|
if not api_key:
|
||||||
raise ValueError("ip_location requires an api_key (inject it; never hardcode)")
|
raise ValueError("ip_location requires an api_key (inject it; never hardcode)")
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
"""pure ip/address utilities over stdlib ipaddress (no network, no deps).
|
"""pure ip/address utilities over stdlib ipaddress (no network, no deps).
|
||||||
|
|
||||||
every function accepts strings. validation/membership helpers
|
validation/membership helpers (`is_valid`/`version`/`in_network`/`in_any`) never
|
||||||
(`is_valid`/`version`/`in_network`/`in_any`) never raise on bad input — they return
|
raise on bad input - they return a falsy value. functions that require a valid
|
||||||
a falsy value. functions that require a valid address or cidr (`to_int`/`set_bits`/
|
address or cidr (`to_int`/`set_bits`/the network-shape helpers) let `ValueError`
|
||||||
the network-shape helpers) let `ValueError` propagate so misuse is visible.
|
propagate so misuse is visible. cidr parsing uses `ip_network(cidr, strict=False)`
|
||||||
|
throughout, so host bits set in the network string are tolerated (e.g. "10.0.0.5/24").
|
||||||
cidr parsing uses `ip_network(cidr, strict=False)` throughout, so host bits set in
|
|
||||||
the network string are tolerated (e.g. "10.0.0.5/24").
|
|
||||||
"""
|
"""
|
||||||
import ipaddress
|
import ipaddress
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
|
|||||||
@@ -1,11 +1,7 @@
|
|||||||
"""display masking for sensitive-looking values.
|
"""display masking for sensitive-looking values (e.g. "•••• •••• •••• 1234").
|
||||||
|
|
||||||
these are DISPLAY helpers only — they format a value for showing in a UI or log
|
DISPLAY only, not a security control: the underlying value is unchanged and still
|
||||||
(e.g. "•••• •••• •••• 1234"). they are not a security control: the underlying
|
needs proper handling (encryption at rest, etc.).
|
||||||
value is unchanged and still needs proper handling (encryption at rest, etc.).
|
|
||||||
|
|
||||||
card helpers: `credit`, `cvv`, `provider`. token: `phantom`. url/proxy redaction
|
|
||||||
for logging connection strings: `mask_url`, `mask_proxy`.
|
|
||||||
"""
|
"""
|
||||||
from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit
|
from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit
|
||||||
|
|
||||||
@@ -35,7 +31,7 @@ def cvv(value: str) -> str:
|
|||||||
def phantom(value: str) -> str:
|
def phantom(value: str) -> str:
|
||||||
"""show a long token as first-six...last-four (e.g. a hash or id)
|
"""show a long token as first-six...last-four (e.g. a hash or id)
|
||||||
|
|
||||||
values of length <= 10 fully mask instead — first6+last4 would otherwise
|
values of length <= 10 fully mask instead - first6+last4 would otherwise
|
||||||
reveal (or double-reveal) every character while still looking masked.
|
reveal (or double-reveal) every character while still looking masked.
|
||||||
"""
|
"""
|
||||||
if len(value) <= 10:
|
if len(value) <= 10:
|
||||||
@@ -46,7 +42,7 @@ def phantom(value: str) -> str:
|
|||||||
def provider(card_number: str) -> str:
|
def provider(card_number: str) -> str:
|
||||||
"""short card brand from the number's prefix, or UNKW if undetermined
|
"""short card brand from the number's prefix, or UNKW if undetermined
|
||||||
|
|
||||||
a BIN-prefix heuristic for display/labeling — not authoritative validation.
|
a BIN-prefix heuristic for display/labeling - not authoritative validation.
|
||||||
tolerates spaces/dashes and short or non-numeric input (returns UNKW).
|
tolerates spaces/dashes and short or non-numeric input (returns UNKW).
|
||||||
"""
|
"""
|
||||||
n = _digits(card_number)
|
n = _digits(card_number)
|
||||||
|
|||||||
@@ -1,12 +1,4 @@
|
|||||||
"""nested access by dotted path.
|
"""nested dict/list access by dotted path (deep_get / deep_set)."""
|
||||||
|
|
||||||
`deep_get(data, "in.this.old.notation")` walks dicts (and lists, when a segment is
|
|
||||||
a number) and returns a default instead of raising on a missing/!wrong path.
|
|
||||||
`deep_set` writes a nested value, creating intermediate dicts and indexing into
|
|
||||||
existing lists on numeric segments (mirroring deep_get), so a get/set round-trip
|
|
||||||
on the same path never corrupts a list. an out-of-range numeric segment raises
|
|
||||||
IndexError rather than silently mis-storing.
|
|
||||||
"""
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
_MISSING = object()
|
_MISSING = object()
|
||||||
|
|||||||
+3
-21
@@ -1,25 +1,7 @@
|
|||||||
"""retry with exponential backoff — sync and async, one backoff engine.
|
"""retry with exponential backoff, sync (`retry`) and async (`aretry`), call or decorator form.
|
||||||
|
|
||||||
de-duplicates retry logic that was written divergently across several libs (HTTP
|
|
||||||
429/5xx caps, proxy burn/rotate caps, IMAP reconnects). both a call form and a
|
|
||||||
decorator form share one implementation per flavor; the backoff schedule is a pure
|
|
||||||
generator so it tests without real sleeps.
|
|
||||||
|
|
||||||
from commons import retry, aretry
|
|
||||||
|
|
||||||
# call form
|
|
||||||
rows = retry(lambda: db_read(), attempts=5, on=(IOError,))
|
|
||||||
data = await aretry(lambda: fetch(url), attempts=3, on=(TimeoutError,))
|
|
||||||
|
|
||||||
# decorator form (same kwargs)
|
|
||||||
@aretry(attempts=4, backoff=0.5, on=(ConnectionError,))
|
|
||||||
async def pull():
|
|
||||||
...
|
|
||||||
|
|
||||||
after `attempts` are exhausted the LAST exception is re-raised (fail loud, never
|
after `attempts` are exhausted the LAST exception is re-raised (fail loud, never
|
||||||
swallowed). `on` narrows which exceptions retry; `give_up(exc) -> bool` stops early
|
swallowed). see README for usage examples.
|
||||||
on a non-retryable error (e.g. a 400 vs a 429). each retry is logged (emit-only),
|
|
||||||
never printed.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
@@ -38,7 +20,7 @@ def _delays(attempts: int, backoff: float, factor: float, max_backoff: float):
|
|||||||
"""yield the wait before each retry: min(backoff * factor**n, max_backoff)
|
"""yield the wait before each retry: min(backoff * factor**n, max_backoff)
|
||||||
|
|
||||||
yields `attempts - 1` delays (one before each retry after the first try). the
|
yields `attempts - 1` delays (one before each retry after the first try). the
|
||||||
raw, un-jittered schedule — jitter is applied at call time so the schedule stays
|
raw, un-jittered schedule - jitter is applied at call time so the schedule stays
|
||||||
pure and testable.
|
pure and testable.
|
||||||
"""
|
"""
|
||||||
for n in range(max(0, attempts - 1)):
|
for n in range(max(0, attempts - 1)):
|
||||||
|
|||||||
+6
-12
@@ -1,22 +1,16 @@
|
|||||||
"""time helpers built on unix timestamps with timezone-aware datetime support.
|
"""time helpers built on unix timestamps with timezone-aware datetime support.
|
||||||
|
|
||||||
one engine, two ergonomics:
|
one engine, two ergonomics: bare module functions for stateless unix-int math
|
||||||
- bare module functions (now/add/ahead/ago/is_expired/to_dt/...) operate on unix
|
(all delta math routes through `_delta_seconds`, the single source of truth), and
|
||||||
ints for quick, stateless use. all delta math routes through `_delta_seconds`.
|
`Clock`, which holds a timezone + fast flag and delegates to the same functions.
|
||||||
- `Clock` holds a timezone + fast flag and delegates to the same functions, so a
|
|
||||||
configured clock and the bare functions never diverge.
|
|
||||||
|
|
||||||
unix ints stay the storable value; datetimes are produced on demand in whatever
|
|
||||||
timezone you ask for (stdlib zoneinfo, no pytz).
|
|
||||||
"""
|
"""
|
||||||
import time as _time
|
import time as _time
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from typing import Optional, Union
|
from typing import Optional, Union
|
||||||
from zoneinfo import ZoneInfo
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
# default fast flag for bare module calls. when True, every unit (day/hour/minute)
|
# default fast flag for bare calls (test-only: collapses each unit to 1s); set
|
||||||
# counts as one second so time-based flows run fast in tests. set
|
# `commons.timing.FAST_MODE` from test setup, off by default, not a config import.
|
||||||
# `commons.timing.FAST_MODE` from test setup; off by default. not a config import.
|
|
||||||
FAST_MODE = False
|
FAST_MODE = False
|
||||||
|
|
||||||
UTC = timezone.utc
|
UTC = timezone.utc
|
||||||
@@ -42,7 +36,7 @@ def now() -> int:
|
|||||||
def _delta_seconds(days: int, hours: int, minutes: int, seconds: int, fast: bool) -> int:
|
def _delta_seconds(days: int, hours: int, minutes: int, seconds: int, fast: bool) -> int:
|
||||||
"""seconds for a unit combination; collapses to 1s/unit when fast is True
|
"""seconds for a unit combination; collapses to 1s/unit when fast is True
|
||||||
|
|
||||||
single source of truth for unit math — every delta helper routes through here.
|
single source of truth for unit math - every delta helper routes through here.
|
||||||
"""
|
"""
|
||||||
if fast:
|
if fast:
|
||||||
return days + hours + minutes + seconds
|
return days + hours + minutes + seconds
|
||||||
|
|||||||
Reference in New Issue
Block a user