feat: mask_url/mask_proxy display maskers; fix: deep_set now indexes into lists on numeric segments (commons-2)

mask_url/mask_proxy redact credentials from connection strings for logging
(display only, not a security control). deep_set previously destroyed a list
when a numeric path segment landed on it (isinstance(nxt, dict) check
replaced the list wholesale); it now mirrors deep_get's list indexing so a
get/set round-trip on the same dotted path never corrupts data, raising
IndexError on an out-of-range segment instead of silently mis-storing.

Signed-off-by: disqualifier <dev@disqualifier.me>
This commit is contained in:
2026-07-02 17:36:03 -04:00
parent 1dc27ebc1a
commit 19e6f4aa06
5 changed files with 124 additions and 18 deletions
+25 -4
View File
@@ -12,15 +12,15 @@ 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.3
commons @ git+ssh://git@git.rethinkstudios.io/rethink-public/commons.git@v0.3.1
# 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.3.1
```
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.
Drop the `@v0.2.3` suffix from the line above to install the latest unpinned.
Drop the `@v0.3.1` suffix from the line above to install the latest unpinned.
## timing
@@ -99,22 +99,43 @@ deep_get(data, "items.1.id") # "b" (numeric segment indexes a list)
deep_get(data, "in.nope.here", "DEF") # "DEF" (missing -> default, no raise)
deep_set({}, "a.b.c", 9) # {"a": {"b": {"c": 9}}}
deep_set(data, "items.1.id", "B") # updates the list element in place
deep_get(data, "items.1.id") # "B" (get/set share the same grammar)
deep_set(data, "items.9.id", "X") # raises IndexError (out of range, no
# silent mis-store)
```
`deep_set` mirrors `deep_get`'s list indexing: a numeric segment over an existing
list/tuple updates that element in place rather than replacing the list with a dict.
An out-of-range numeric segment raises `IndexError` instead of silently corrupting
the structure.
## masking
Display helpers only — they format a value for showing; they are **not a security
control** (the underlying value is unchanged and still needs proper handling).
```python
from commons import credit, cvv, phantom, provider
from commons import credit, cvv, phantom, provider, mask_url, mask_proxy
credit("4111 1111 1111 1234") # "•••• •••• •••• 1234"
cvv("123") # "•••"
phantom("abcdef1234567890") # "abcdef...7890"
provider("4111111111111111") # "VISA" (VISA/MC/AMEX/UPAY/DISC/JCB/DNRS/UNKW)
# redact credentials in a connection string before logging it
mask_url("https://u:pw@api.x/v2?apiKey=SECRET&ip=8.8.8.8")
# -> "https://api.x/v2?apiKey=***&ip=8.8.8.8" (userinfo dropped, secret query masked)
mask_url("redis://:pw@127.0.0.1:6379/0") # -> "redis://127.0.0.1:6379/0"
mask_proxy("1.2.3.4:8080:user:supersecret") # -> "1.2.3.4:8080:user:****"
mask_proxy("1.2.3.4:8080") # -> "1.2.3.4:8080" (no auth, untouched)
```
`mask_url` strips `user:pass@` userinfo and replaces the values of sensitive query
params (`apiKey`, `token`, `password`, `secret`, …; override via `keys=`) with `***`.
`mask_proxy` bullets the password of a `host:port:user:password` spec. Non-URL /
non-conforming input is returned unchanged.
## retry
Exponential-backoff retry, sync (`retry`) and async (`aretry`). Each works as a **call