unstd 1.1.0__tar.gz → 2.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {unstd-1.1.0 → unstd-2.0.0}/CHANGELOG.md +169 -0
- {unstd-1.1.0 → unstd-2.0.0}/PKG-INFO +4 -5
- {unstd-1.1.0 → unstd-2.0.0}/README.md +2 -2
- {unstd-1.1.0 → unstd-2.0.0}/bench/README.md +2 -2
- {unstd-1.1.0 → unstd-2.0.0}/bench/baseline.json +3 -3
- {unstd-1.1.0 → unstd-2.0.0}/bench/cases.py +10 -9
- {unstd-1.1.0 → unstd-2.0.0}/pyproject.toml +2 -3
- {unstd-1.1.0 → unstd-2.0.0}/src/unstd/__init__.py +12 -9
- unstd-2.0.0/src/unstd/_extra.py +28 -0
- {unstd-1.1.0 → unstd-2.0.0}/src/unstd/audio/README.md +11 -11
- {unstd-1.1.0 → unstd-2.0.0}/src/unstd/audio/wavx.py +85 -53
- {unstd-1.1.0 → unstd-2.0.0}/src/unstd/clone/structural.py +6 -1
- {unstd-1.1.0 → unstd-2.0.0}/src/unstd/containers/persistent.py +4 -4
- {unstd-1.1.0 → unstd-2.0.0}/src/unstd/containers/sorted.py +6 -5
- {unstd-1.1.0 → unstd-2.0.0}/src/unstd/crypto/README.md +17 -14
- unstd-2.0.0/src/unstd/crypto/__init__.py +20 -0
- unstd-2.0.0/src/unstd/crypto/_blake3.py +19 -0
- {unstd-1.1.0 → unstd-2.0.0}/src/unstd/crypto/digest.py +53 -80
- {unstd-1.1.0 → unstd-2.0.0}/src/unstd/crypto/tls.py +11 -6
- {unstd-1.1.0 → unstd-2.0.0}/src/unstd/crypto/token.py +46 -51
- {unstd-1.1.0 → unstd-2.0.0}/src/unstd/fs/README.md +5 -3
- {unstd-1.1.0 → unstd-2.0.0}/src/unstd/fs/__init__.py +44 -47
- {unstd-1.1.0 → unstd-2.0.0}/src/unstd/ids/README.md +6 -9
- {unstd-1.1.0 → unstd-2.0.0}/src/unstd/ids/__init__.py +5 -5
- {unstd-1.1.0 → unstd-2.0.0}/src/unstd/ids/uid.py +43 -41
- {unstd-1.1.0 → unstd-2.0.0}/src/unstd/iters/README.md +6 -6
- {unstd-1.1.0 → unstd-2.0.0}/src/unstd/iters/__init__.py +2 -2
- unstd-1.1.0/src/unstd/iters/runs.py → unstd-2.0.0/src/unstd/iters/consecutive.py +17 -8
- {unstd-1.1.0 → unstd-2.0.0}/src/unstd/iters/distinct.py +24 -10
- unstd-2.0.0/src/unstd/iters/reshape.py +52 -0
- {unstd-1.1.0 → unstd-2.0.0}/src/unstd/iters/select.py +10 -2
- {unstd-1.1.0 → unstd-2.0.0}/src/unstd/pack/binpack.py +5 -4
- {unstd-1.1.0 → unstd-2.0.0}/src/unstd/proc/README.md +4 -3
- {unstd-1.1.0 → unstd-2.0.0}/src/unstd/proc/__init__.py +6 -8
- {unstd-1.1.0 → unstd-2.0.0}/src/unstd/rand/__init__.py +6 -6
- {unstd-1.1.0 → unstd-2.0.0}/src/unstd/rand/crypto.py +12 -28
- {unstd-1.1.0 → unstd-2.0.0}/src/unstd/rand/draw.py +36 -57
- {unstd-1.1.0 → unstd-2.0.0}/src/unstd/rex/README.md +17 -12
- {unstd-1.1.0 → unstd-2.0.0}/src/unstd/rex/__init__.py +101 -80
- {unstd-1.1.0 → unstd-2.0.0}/src/unstd/serde/README.md +14 -11
- unstd-2.0.0/src/unstd/serde/b64.py +127 -0
- {unstd-1.1.0 → unstd-2.0.0}/src/unstd/serde/jsonx.py +61 -44
- {unstd-1.1.0 → unstd-2.0.0}/src/unstd/serde/ndjson.py +50 -22
- {unstd-1.1.0 → unstd-2.0.0}/src/unstd/serde/structs.py +19 -40
- {unstd-1.1.0 → unstd-2.0.0}/src/unstd/text/fuzz.py +141 -117
- unstd-2.0.0/src/unstd/time/README.md +69 -0
- unstd-2.0.0/src/unstd/time/__init__.py +15 -0
- unstd-2.0.0/src/unstd/time/timeutil.py +292 -0
- unstd-2.0.0/src/unstd/time/zoned.py +133 -0
- {unstd-1.1.0 → unstd-2.0.0}/src/unstd/toml/README.md +1 -1
- unstd-2.0.0/src/unstd/toml/__init__.py +129 -0
- {unstd-1.1.0 → unstd-2.0.0}/tests/audio/test_audio.py +74 -49
- {unstd-1.1.0 → unstd-2.0.0}/tests/conftest.py +3 -3
- {unstd-1.1.0 → unstd-2.0.0}/tests/containers/test_containers.py +3 -3
- {unstd-1.1.0 → unstd-2.0.0}/tests/crypto/test_adverse.py +51 -41
- {unstd-1.1.0 → unstd-2.0.0}/tests/crypto/test_vectors.py +15 -15
- {unstd-1.1.0 → unstd-2.0.0}/tests/fs/test_fs.py +28 -26
- {unstd-1.1.0 → unstd-2.0.0}/tests/iters/test_iters.py +79 -12
- {unstd-1.1.0 → unstd-2.0.0}/tests/proc/test_proc.py +8 -5
- {unstd-1.1.0 → unstd-2.0.0}/tests/properties/test_toml_invariants.py +13 -6
- {unstd-1.1.0 → unstd-2.0.0}/tests/properties/test_wav_quant.py +2 -2
- {unstd-1.1.0 → unstd-2.0.0}/tests/rex/test_rex.py +102 -0
- unstd-2.0.0/tests/serde/test_b64.py +135 -0
- {unstd-1.1.0 → unstd-2.0.0}/tests/serde/test_ndjson.py +56 -18
- {unstd-1.1.0 → unstd-2.0.0}/tests/serde/test_structs.py +19 -19
- {unstd-1.1.0 → unstd-2.0.0}/tests/test_package.py +1 -1
- {unstd-1.1.0 → unstd-2.0.0}/tests/text/test_text.py +42 -1
- {unstd-1.1.0 → unstd-2.0.0}/tests/time/test_timeutil.py +170 -47
- unstd-2.0.0/tests/time/test_zoned.py +172 -0
- {unstd-1.1.0 → unstd-2.0.0}/tests/toml/test_toml.py +36 -18
- {unstd-1.1.0 → unstd-2.0.0}/uv.lock +10 -12
- unstd-1.1.0/src/unstd/containers/guard.py +0 -48
- unstd-1.1.0/src/unstd/crypto/__init__.py +0 -34
- unstd-1.1.0/src/unstd/ids/hash.py +0 -39
- unstd-1.1.0/src/unstd/iters/reshape.py +0 -56
- unstd-1.1.0/src/unstd/serde/b64.py +0 -142
- unstd-1.1.0/src/unstd/time/README.md +0 -47
- unstd-1.1.0/src/unstd/time/__init__.py +0 -14
- unstd-1.1.0/src/unstd/time/dateutil.py +0 -194
- unstd-1.1.0/src/unstd/time/timeutil.py +0 -219
- unstd-1.1.0/src/unstd/toml/__init__.py +0 -201
- unstd-1.1.0/tests/serde/test_b64.py +0 -117
- unstd-1.1.0/tests/time/test_dateutil.py +0 -305
- {unstd-1.1.0 → unstd-2.0.0}/.github/workflows/ci.yml +0 -0
- {unstd-1.1.0 → unstd-2.0.0}/.github/workflows/release.yml +0 -0
- {unstd-1.1.0 → unstd-2.0.0}/.gitignore +0 -0
- {unstd-1.1.0 → unstd-2.0.0}/LICENSE +0 -0
- {unstd-1.1.0 → unstd-2.0.0}/NOTICE +0 -0
- {unstd-1.1.0 → unstd-2.0.0}/bench/__init__.py +0 -0
- {unstd-1.1.0 → unstd-2.0.0}/bench/__main__.py +0 -0
- {unstd-1.1.0 → unstd-2.0.0}/bench/report.py +0 -0
- {unstd-1.1.0 → unstd-2.0.0}/bench/timing.py +0 -0
- {unstd-1.1.0 → unstd-2.0.0}/src/unstd/audio/__init__.py +0 -0
- {unstd-1.1.0 → unstd-2.0.0}/src/unstd/clone/README.md +0 -0
- {unstd-1.1.0 → unstd-2.0.0}/src/unstd/clone/__init__.py +0 -0
- {unstd-1.1.0 → unstd-2.0.0}/src/unstd/containers/README.md +0 -0
- {unstd-1.1.0 → unstd-2.0.0}/src/unstd/containers/__init__.py +0 -0
- {unstd-1.1.0 → unstd-2.0.0}/src/unstd/model/README.md +0 -0
- {unstd-1.1.0 → unstd-2.0.0}/src/unstd/model/__init__.py +0 -0
- {unstd-1.1.0 → unstd-2.0.0}/src/unstd/model/base.py +0 -0
- {unstd-1.1.0 → unstd-2.0.0}/src/unstd/model/settings.py +0 -0
- {unstd-1.1.0 → unstd-2.0.0}/src/unstd/pack/README.md +0 -0
- {unstd-1.1.0 → unstd-2.0.0}/src/unstd/pack/__init__.py +0 -0
- {unstd-1.1.0 → unstd-2.0.0}/src/unstd/py.typed +0 -0
- {unstd-1.1.0 → unstd-2.0.0}/src/unstd/rand/README.md +0 -0
- {unstd-1.1.0 → unstd-2.0.0}/src/unstd/serde/__init__.py +0 -0
- {unstd-1.1.0 → unstd-2.0.0}/src/unstd/text/README.md +0 -0
- {unstd-1.1.0 → unstd-2.0.0}/src/unstd/text/__init__.py +0 -0
- {unstd-1.1.0 → unstd-2.0.0}/tests/clone/test_clone.py +0 -0
- {unstd-1.1.0 → unstd-2.0.0}/tests/crypto/blake3_reference_vectors.json +0 -0
- {unstd-1.1.0 → unstd-2.0.0}/tests/ids/test_uid.py +0 -0
- {unstd-1.1.0 → unstd-2.0.0}/tests/model/test_model.py +0 -0
- {unstd-1.1.0 → unstd-2.0.0}/tests/pack/test_pack.py +0 -0
- {unstd-1.1.0 → unstd-2.0.0}/tests/properties/README.md +0 -0
- {unstd-1.1.0 → unstd-2.0.0}/tests/properties/test_clone_invariants.py +0 -0
- {unstd-1.1.0 → unstd-2.0.0}/tests/properties/test_pack_invariants.py +0 -0
- {unstd-1.1.0 → unstd-2.0.0}/tests/properties/test_serde_parity.py +0 -0
- {unstd-1.1.0 → unstd-2.0.0}/tests/properties/test_uuid7.py +0 -0
- {unstd-1.1.0 → unstd-2.0.0}/tests/rand/test_rand.py +0 -0
- {unstd-1.1.0 → unstd-2.0.0}/tests/serde/test_jsonx.py +0 -0
- {unstd-1.1.0 → unstd-2.0.0}/tests/test_bench.py +0 -0
|
@@ -2,6 +2,175 @@
|
|
|
2
2
|
|
|
3
3
|
<!-- towncrier release notes start -->
|
|
4
4
|
|
|
5
|
+
## [2.0.0] - 2026-09-27
|
|
6
|
+
|
|
7
|
+
### Breaking
|
|
8
|
+
|
|
9
|
+
This is a 2.0 surface pass: every change below renames or removes a public name,
|
|
10
|
+
and each old name fails at import rather than changing meaning in place.
|
|
11
|
+
|
|
12
|
+
- `serde.b64` is one verb per direction. `b64s` / `b64u_s` → `encode(data, url=,
|
|
13
|
+
pad=)`, `b64d` / `b64u_d` → `decode(s, url=)`, `b64text` / `b64u_text` →
|
|
14
|
+
`decode_text`, `b64_json` / `unb64_json` → `encode_json` / `decode_json`.
|
|
15
|
+
`pad=False` is new - the unpadded base64url JWTs and bearer tokens use, which
|
|
16
|
+
callers were `rstrip("=")`-ing by hand - and url-safe `decode` now accepts
|
|
17
|
+
padded and unpadded input alike. Standard `decode` stays strict.
|
|
18
|
+
- `ids.hash` is gone; it was three second names for `crypto.digest`. Content
|
|
19
|
+
hashes are `digest.hex(data)`, short ids `digest.hex(data, n)`, raw bytes
|
|
20
|
+
`digest.raw(data)` (was `sum_` / `hex_` / `hex_n` / `content_hash`).
|
|
21
|
+
`digest.of_parts(parts)` is `digest.Stream(parts).raw()`; `Stream.update_all`
|
|
22
|
+
went with it, and `Stream.sum_` / `hex_` are `raw` / `hex(n=None)`.
|
|
23
|
+
`digest.KDF_BYTES` folded into `DIGEST_BYTES` (both were 32).
|
|
24
|
+
- `crypto.token.mac` / `mac_bytes` → `mac_hex` / `mac_raw`, so the return type is
|
|
25
|
+
in the name as it is in `digest`. `verify_mac` takes a tag in either form.
|
|
26
|
+
`equal` lives in `token` only, and `token.opaque` is
|
|
27
|
+
`rand.crypto.token_urlsafe` - it was a pass-through.
|
|
28
|
+
- `time.dateutil` is `time.zoned`, holding only what needs `whenever`:
|
|
29
|
+
`wall` / `parse` / `now` / `instant` / `to_utc` (were `zoned_wall` /
|
|
30
|
+
`parse_zoned` / `zoned_now` / `parse_instant` / `zoned_to_utc`). The pure-stdlib
|
|
31
|
+
half - `parse_iso`, `parse_utc`, `parse_date`, `from_epoch_s`, `from_epoch_ms` -
|
|
32
|
+
moved to `timeutil`, so none of it needs the `time` extra any more.
|
|
33
|
+
- `timeutil.span(start, end=None, *, unit, assume_utc=False)` replaces
|
|
34
|
+
`span_hours`, `span_minutes`, `age_days`, `minutes_until`, `minutes_since`, and
|
|
35
|
+
`hours_between`. Those five disagreed about whether a floating stamp was UTC
|
|
36
|
+
depending on which unit you asked in; `span` is strict unless told otherwise.
|
|
37
|
+
`iso_range`, `lookback`, `days_range`, and `day_offset` are removed.
|
|
38
|
+
- `timeutil.iso(dt=None, *, ms=False)` replaces `utcnow_iso`, `utcnow_iso_ms`,
|
|
39
|
+
and `iso_fmt`; `today(utc=True)` replaces `utcnow_date_iso`; `today_iso` and
|
|
40
|
+
`date_fmt` are removed (`.isoformat()`); `human(tz="")` replaces `human_now`
|
|
41
|
+
and `human_now_tz`.
|
|
42
|
+
- `timeutil.parse_date` returns `None` on bad input like every other parser
|
|
43
|
+
here, instead of raising.
|
|
44
|
+
- `serde.structs.codec(...)` is removed; construct `structs.Codec(...)`.
|
|
45
|
+
- `audio.wavx` drops the stutter: `read_wav` / `write_wav` / `read_blocks` /
|
|
46
|
+
`WavWriter` → `read` / `write` / `blocks` / `Writer`.
|
|
47
|
+
- `fs.read_bytes`, `fs.read_text`, and `fs.iter_dir` are removed. They measured
|
|
48
|
+
slower than the `pathlib` methods they wrapped.
|
|
49
|
+
- `proc.TimeoutError` is removed. It aliased `subprocess.TimeoutExpired`, which
|
|
50
|
+
does not subclass the builtin of that name.
|
|
51
|
+
- `iters.take(iterable, n)` takes the iterable first, like `chunked`, `windowed`,
|
|
52
|
+
and `nth` beside it. The old order fails loudly (`'int' object is not
|
|
53
|
+
iterable`), never silently.
|
|
54
|
+
- `iters.batched_with_key` is `iters.runs` - it groups consecutive runs by key;
|
|
55
|
+
it never batched. Its module is `iters.consecutive`.
|
|
56
|
+
- `iters.partition(iterable, pred)` takes the iterable first, like every other
|
|
57
|
+
combinator in `iters`. The old order raises `TypeError` at the call.
|
|
58
|
+
- `serde.ndjson.read(fp)` / `write(fp, rows)` → `load(fp)` / `dump(rows, fp)`,
|
|
59
|
+
the same shape as `jsonx.dump(obj, fp)` and `toml.dump(obj, fp)`. `dump` still
|
|
60
|
+
returns the record count.
|
|
61
|
+
- `timeutil.now_tz(tz)` requires its zone. Called bare it was a second spelling
|
|
62
|
+
of `utcnow()`.
|
|
63
|
+
- `toml`'s write half is `tomlkit`'s own functions rather than wrappers around
|
|
64
|
+
them, so `dumps` / `dump` accept `sort_keys=` and each name carries tomlkit's
|
|
65
|
+
types. On a base install each is a placeholder that raises naming the extra,
|
|
66
|
+
decided at import rather than re-checked per call.
|
|
67
|
+
- `unstd.crypto` no longer re-exports the `blake3` class; it was the backend,
|
|
68
|
+
not a surface. The guard moved into `digest` / `token`, so `crypto.tls` - pure
|
|
69
|
+
stdlib - now imports on a base install instead of failing on a missing BLAKE3.
|
|
70
|
+
- `crypto.tls.client_context(certfile=...)` alone loads the chain, which is the
|
|
71
|
+
combined cert-plus-key PEM most bundles ship as; it used to be ignored and the
|
|
72
|
+
client dialed without mTLS. A `keyfile` with no `certfile` raises `ValueError`.
|
|
73
|
+
- The `ids` extra no longer pulls `blake3`; nothing under `ids` has used it since
|
|
74
|
+
`ids.hash` folded into `crypto.digest`. Depend on `unstd[crypto]` for digests.
|
|
75
|
+
- `unstd.containers.guard` is gone. Its placeholder and `toml`'s copy of it are
|
|
76
|
+
one private helper, and every missing-backend name now raises the same
|
|
77
|
+
`unstd.<module>.<name> requires the '<extra>' extra` message.
|
|
78
|
+
|
|
79
|
+
### Performance
|
|
80
|
+
|
|
81
|
+
- `jsonx.dumps` 416 → 264 ns, `dumpb` 375 → 200 ns, `loads` 251 → 212 ns on a
|
|
82
|
+
small dict (raw orjson: ~180 ns). The fast path no longer detours through a
|
|
83
|
+
checking wrapper, and `loads` names its return type with an annotation instead
|
|
84
|
+
of a runtime `typing.cast` call.
|
|
85
|
+
- `ndjson.dumpb` / binary `write` let orjson append each newline
|
|
86
|
+
(`OPT_APPEND_NEWLINE`) instead of concatenating a second `bytes` per row.
|
|
87
|
+
- `rand`'s scalar draws are stdlib `random` on every install. Routing
|
|
88
|
+
`random()` / `choice` / `shuffle` through numpy's `Generator` one value at a
|
|
89
|
+
time was 5-7x slower than the stdlib draw; numpy still fills the bulk arrays.
|
|
90
|
+
- `timeutil.wall` / `mono` / `mono_ns` / `perf` and `rand.crypto.*` are the stdlib
|
|
91
|
+
functions bound directly, with no forwarding frame.
|
|
92
|
+
- `timeutil.iso` formats through `isoformat` (~2.6x faster than `strftime`), and
|
|
93
|
+
`epoch_ms` is exact integer math on `time_ns()`.
|
|
94
|
+
- `structs.Codec.encode` / `decode` are msgspec's own bound methods.
|
|
95
|
+
- `digest.hex(data, n)` asks the XOF for `n` bytes rather than slicing a full
|
|
96
|
+
hex string, and checks the width inline.
|
|
97
|
+
- `jsonx.canonical` reuses one prebuilt encoder instead of letting `json.dumps`
|
|
98
|
+
construct a fresh one per call: ~1.25 µs → ~0.9 µs on a small dict, same bytes.
|
|
99
|
+
- `b64.encode(url=True)` rides pybase64's `altchars` straight to `str`: 293 →
|
|
100
|
+
192 ns for 64 bytes unpadded.
|
|
101
|
+
- `token.mac_hex` / `mac_raw` check the key inline - 943 → 674 ns - and `verify`
|
|
102
|
+
follows (2.1 → 1.6 µs).
|
|
103
|
+
- `ndjson.loads` parses a clean blob in one comprehension over the backend
|
|
104
|
+
decoder and strips a line only when a direct parse fails: 338 → 187 µs per
|
|
105
|
+
1k records, level with a bare orjson loop. A bad line still re-walks the slow
|
|
106
|
+
path, so the error and its line number are unchanged.
|
|
107
|
+
- `iters.windowed(step=1)` is staggered `tee` views zipped together, a C-level
|
|
108
|
+
walk: 4x on 10k items. `unique_everseen` skips the key call when there is no
|
|
109
|
+
key, and `last` indexes exact builtin sequences without an ABC probe (2x).
|
|
110
|
+
- Pure forwarders are now the function they forwarded to: `timeutil.utcnow`
|
|
111
|
+
(`partial(datetime.now, UTC)`, 132 → 96 ns), `uid.parse` / `uid.derive`
|
|
112
|
+
(`UUID` / `uuid5`), `iters.flatten` (`chain.from_iterable`, 2x), `zoned.now`
|
|
113
|
+
(`ZonedDateTime.now`), and - with the `text` extra - `fuzz.ratio` /
|
|
114
|
+
`distance` / `jaro_winkler` as the rapidfuzz kernels (~25-40% per call). Clock
|
|
115
|
+
freezers that patch at the C layer (`time-machine`) still see the bound
|
|
116
|
+
references. The pure-Python fallbacks are held to the kernels by a property
|
|
117
|
+
test, since with the extra installed nothing else runs them. The fuzz scorers
|
|
118
|
+
take their two strings positionally.
|
|
119
|
+
- `wavx.wrap_pcm` packs the 44-byte header with one `struct` call - byte-identical
|
|
120
|
+
to `wave`, pinned across widths, channel counts, and partial frames - 2.7x
|
|
121
|
+
faster on 32 KB.
|
|
122
|
+
- `iters.partition` is the current `itertools` recipe - verdicts teed beside the
|
|
123
|
+
values and split with `compress` - so `pred` runs once per element instead of
|
|
124
|
+
once per side: 533 → 459 µs on 10k items with a trivial predicate, and half the
|
|
125
|
+
predicate calls whatever it costs.
|
|
126
|
+
- `ndjson.dumps` joins orjson's row bytes and decodes once, and a text-mode
|
|
127
|
+
`dump` decodes each row's bytes rather than re-entering `jsonx.dumps`: ~20%
|
|
128
|
+
on both (399 → 317 µs and 547 → 421 µs per 1k rows).
|
|
129
|
+
- `clone.asdict` tests dict keys and values for atoms inline instead of paying a
|
|
130
|
+
call per leaf: 5.2 → 3.8 µs on a dict-heavy record.
|
|
131
|
+
- `rand.choices` unboxes the drawn index array once instead of calling `int()`
|
|
132
|
+
per element: 379 → 209 µs for 10k draws.
|
|
133
|
+
|
|
134
|
+
### Fixed
|
|
135
|
+
|
|
136
|
+
- `text.fuzz.best_match` / `extract` / `cdist` keep a choice scoring exactly at
|
|
137
|
+
`score_cutoff`, as documented. rapidfuzz's own cutoff drops boundary pairs
|
|
138
|
+
through a float round-trip (about 15% of random short pairs on 1.1), so every
|
|
139
|
+
fast path now scores in full and applies `>=` itself - at no measurable cost.
|
|
140
|
+
`cdist` also returns float64, so its scores equal `ratio`'s exactly.
|
|
141
|
+
- `fs.atomic_write` / `atomic_writer` no longer flip the process-wide umask to
|
|
142
|
+
read it when creating a new file. Setting it to 0 and back left a window in
|
|
143
|
+
which any other thread creating a file got mode `0o666`/`0o777`. The temp file
|
|
144
|
+
is now opened at `0o666` so the kernel applies the umask, and an existing
|
|
145
|
+
destination's mode is still copied over.
|
|
146
|
+
|
|
147
|
+
- `rex` accepts its own compiled patterns. On the irgx path `rex.compile(p)` and
|
|
148
|
+
`rex.search(p, s)` raised `TypeError` for a `p` this module had just returned,
|
|
149
|
+
because only stdlib's `re.Pattern` was recognized; stdlib takes either.
|
|
150
|
+
- `rex.purge` clears irgx's pattern cache too, not just stdlib's.
|
|
151
|
+
- A pattern irgx declines (backreferences, lookaround) is no longer re-attempted
|
|
152
|
+
on every call. irgx caches what it compiles, not what it refuses, so each
|
|
153
|
+
`rex.search(r"(\w+)\s+\1", ...)` paid a full compile attempt plus a raise -
|
|
154
|
+
about 1.8 ms, against 0.2 µs for stdlib.
|
|
155
|
+
|
|
156
|
+
### Changed
|
|
157
|
+
|
|
158
|
+
- `rex` keeps a bounded front cache like stdlib's `re._cache`, and hands flags to
|
|
159
|
+
irgx as its keyword arguments instead of building an inline `(?imsx-u)` prefix
|
|
160
|
+
through `RegexFlag` arithmetic. A module-level `rex.search` drops from ~5 µs to
|
|
161
|
+
~0.8 µs - now twice as fast as `re.search` - and a compiled pattern's
|
|
162
|
+
`.pattern` is the caller's source again rather than carrying the prefix.
|
|
163
|
+
- `rex.findall` rewrites irgx's `None` for an absent group only when one is
|
|
164
|
+
actually present, found by a C-level containment scan instead of rebuilding
|
|
165
|
+
every row. A two-group `findall` over 10 KB drops from ~940 µs to ~240 µs.
|
|
166
|
+
- The `rex` extra requires `irregex>=2.6.0`. Below it, a replacement template's
|
|
167
|
+
octal escapes (`\07`, `\101`) and unknown non-letter escapes rendered different
|
|
168
|
+
text than stdlib with no error; 2.6.0 reads them the way `re` does. It also
|
|
169
|
+
makes a group-reading `rex.sub` and a grouped `rex.split` one call into the
|
|
170
|
+
engine instead of one per match, and stops a capped `subn`/`split` walking past
|
|
171
|
+
its cap - on 10 KB, templated `sub` goes from ~7x slower than `re.sub` to
|
|
172
|
+
about even, and `subn(..., count=5)` from ~130x to ~1.7x.
|
|
173
|
+
|
|
5
174
|
## [1.1.0] - 2026-09-27
|
|
6
175
|
|
|
7
176
|
### Fixed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: unstd
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.0.0
|
|
4
4
|
Summary: stdlib-faithful accelerated replacements behind optional extras
|
|
5
5
|
Project-URL: Homepage, https://github.com/The-Billy-Company/unstd
|
|
6
6
|
Project-URL: Repository, https://github.com/The-Billy-Company/unstd
|
|
@@ -30,7 +30,6 @@ Requires-Dist: sortedcontainers>=2.4; extra == 'containers'
|
|
|
30
30
|
Provides-Extra: crypto
|
|
31
31
|
Requires-Dist: blake3>=1.0; extra == 'crypto'
|
|
32
32
|
Provides-Extra: ids
|
|
33
|
-
Requires-Dist: blake3>=1.0; extra == 'ids'
|
|
34
33
|
Requires-Dist: uuid-utils>=0.17; extra == 'ids'
|
|
35
34
|
Provides-Extra: model
|
|
36
35
|
Requires-Dist: pydantic-settings>=2.15; extra == 'model'
|
|
@@ -42,7 +41,7 @@ Requires-Dist: protobuf>=7.36; extra == 'proto'
|
|
|
42
41
|
Provides-Extra: rand
|
|
43
42
|
Requires-Dist: numpy>=2.5; extra == 'rand'
|
|
44
43
|
Provides-Extra: rex
|
|
45
|
-
Requires-Dist: irregex>=2.
|
|
44
|
+
Requires-Dist: irregex>=2.6.0; extra == 'rex'
|
|
46
45
|
Provides-Extra: serde
|
|
47
46
|
Requires-Dist: msgspec>=0.21; extra == 'serde'
|
|
48
47
|
Requires-Dist: orjson>=3.12; extra == 'serde'
|
|
@@ -84,9 +83,9 @@ python3 -m pip install 'unstd[serde,ids,crypto]'
|
|
|
84
83
|
| Group | Extra | Backend / fallback |
|
|
85
84
|
|---|---|---|
|
|
86
85
|
| [`serde`](src/unstd/serde/README.md) | `unstd[serde]` | orjson / msgspec / pybase64 → stdlib `json`/`base64` |
|
|
87
|
-
| [`ids`](src/unstd/ids/README.md) | `unstd[ids]` |
|
|
86
|
+
| [`ids`](src/unstd/ids/README.md) | `unstd[ids]` | uuid-utils → `uid` falls back to stdlib UUIDv7 |
|
|
88
87
|
| [`crypto`](src/unstd/crypto/README.md) | `unstd[crypto]` | blake3 (required; hashlib ships BLAKE2, a different algorithm) |
|
|
89
|
-
| [`time`](src/unstd/time/README.md) | `unstd[time]` | `timeutil` is pure stdlib; `
|
|
88
|
+
| [`time`](src/unstd/time/README.md) | `unstd[time]` | `timeutil` is pure stdlib; `zoned` requires `whenever`, and `timeutil`'s protobuf `Timestamp` bridge requires `unstd[proto]` |
|
|
90
89
|
| [`model`](src/unstd/model/README.md) | `unstd[model]` | pydantic (required) |
|
|
91
90
|
| [`rex`](src/unstd/rex/README.md) | `unstd[rex]` | irregex → stdlib `re` |
|
|
92
91
|
| [`containers`](src/unstd/containers/README.md) | `unstd[containers]` | sortedcontainers + immutables (required) |
|
|
@@ -27,9 +27,9 @@ python3 -m pip install 'unstd[serde,ids,crypto]'
|
|
|
27
27
|
| Group | Extra | Backend / fallback |
|
|
28
28
|
|---|---|---|
|
|
29
29
|
| [`serde`](src/unstd/serde/README.md) | `unstd[serde]` | orjson / msgspec / pybase64 → stdlib `json`/`base64` |
|
|
30
|
-
| [`ids`](src/unstd/ids/README.md) | `unstd[ids]` |
|
|
30
|
+
| [`ids`](src/unstd/ids/README.md) | `unstd[ids]` | uuid-utils → `uid` falls back to stdlib UUIDv7 |
|
|
31
31
|
| [`crypto`](src/unstd/crypto/README.md) | `unstd[crypto]` | blake3 (required; hashlib ships BLAKE2, a different algorithm) |
|
|
32
|
-
| [`time`](src/unstd/time/README.md) | `unstd[time]` | `timeutil` is pure stdlib; `
|
|
32
|
+
| [`time`](src/unstd/time/README.md) | `unstd[time]` | `timeutil` is pure stdlib; `zoned` requires `whenever`, and `timeutil`'s protobuf `Timestamp` bridge requires `unstd[proto]` |
|
|
33
33
|
| [`model`](src/unstd/model/README.md) | `unstd[model]` | pydantic (required) |
|
|
34
34
|
| [`rex`](src/unstd/rex/README.md) | `unstd[rex]` | irregex → stdlib `re` |
|
|
35
35
|
| [`containers`](src/unstd/containers/README.md) | `unstd[containers]` | sortedcontainers + immutables (required) |
|
|
@@ -92,8 +92,8 @@ The harness earned its keep on the first run:
|
|
|
92
92
|
real value is the shape beside it: handed an ndarray it already owns,
|
|
93
93
|
`pack_array` is a memcpy and wins **159×**, and `unpack_array` returns a
|
|
94
94
|
zero-copy view for **1667×**. Hold your data in an array, not a list.
|
|
95
|
-
- **`
|
|
96
|
-
`hashlib` ships from the BLAKE family, so it is the thing `
|
|
95
|
+
- **`digest.raw`'s honest twin is `blake2b`, not `sha256`.** BLAKE2 is what
|
|
96
|
+
`hashlib` ships from the BLAKE family, so it is the thing `digest.raw` stands in for,
|
|
97
97
|
and BLAKE3 beats it 1.7×. `sha256` is not a fair comparison in either
|
|
98
98
|
direction: every ARMv8 and modern x86 core implements it in silicon, so
|
|
99
99
|
`hashlib.sha256` is a hardware instruction and beats single-threaded BLAKE3
|
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
"recorded": "2026-09-27",
|
|
4
4
|
"host": "Darwin arm64 python3.13",
|
|
5
5
|
"cases": {
|
|
6
|
-
"b64.
|
|
6
|
+
"b64.decode · 128 KiB": {
|
|
7
7
|
"floor": 6.42,
|
|
8
8
|
"measured": 8.94,
|
|
9
9
|
"group": "serde"
|
|
10
10
|
},
|
|
11
|
-
"b64.
|
|
11
|
+
"b64.encode · 128 KiB": {
|
|
12
12
|
"floor": 11.77,
|
|
13
13
|
"measured": 16.52,
|
|
14
14
|
"group": "serde"
|
|
@@ -78,7 +78,7 @@
|
|
|
78
78
|
"measured": 41.87,
|
|
79
79
|
"group": "text"
|
|
80
80
|
},
|
|
81
|
-
"
|
|
81
|
+
"digest.raw · 1 MiB": {
|
|
82
82
|
"floor": 1.21,
|
|
83
83
|
"measured": 1.72,
|
|
84
84
|
"group": "ids"
|
|
@@ -37,13 +37,14 @@ from typing import TYPE_CHECKING, Any
|
|
|
37
37
|
|
|
38
38
|
from unstd.clone import asdict as clone_asdict
|
|
39
39
|
from unstd.clone import deep
|
|
40
|
-
from unstd.
|
|
40
|
+
from unstd.crypto.digest import raw as digest_raw
|
|
41
41
|
from unstd.pack.binpack import pack_array, unpack_array
|
|
42
42
|
from unstd.rand.draw import floats, ints
|
|
43
43
|
from unstd.rex import compile as rex_compile
|
|
44
44
|
from unstd.rex import findall as rex_findall
|
|
45
45
|
from unstd.rex import search as rex_search
|
|
46
|
-
from unstd.serde.b64 import
|
|
46
|
+
from unstd.serde.b64 import decode as b64_decode
|
|
47
|
+
from unstd.serde.b64 import encode as b64_encode
|
|
47
48
|
from unstd.serde.jsonx import dumps as jx_dumps
|
|
48
49
|
from unstd.serde.jsonx import loads as jx_loads
|
|
49
50
|
from unstd.text.fuzz import cdist, extract, ratio
|
|
@@ -220,20 +221,20 @@ case(
|
|
|
220
221
|
base=json.loads,
|
|
221
222
|
)
|
|
222
223
|
case(
|
|
223
|
-
name="b64.
|
|
224
|
+
name="b64.encode · 128 KiB",
|
|
224
225
|
group="serde",
|
|
225
226
|
backend="pybase64",
|
|
226
227
|
payload=lambda: _BLOB,
|
|
227
|
-
accel=
|
|
228
|
+
accel=b64_encode,
|
|
228
229
|
base=lambda p: base64.b64encode(p).decode("ascii"),
|
|
229
230
|
note="pybase64 SIMD vs stdlib base64",
|
|
230
231
|
)
|
|
231
232
|
case(
|
|
232
|
-
name="b64.
|
|
233
|
+
name="b64.decode · 128 KiB",
|
|
233
234
|
group="serde",
|
|
234
235
|
backend="pybase64",
|
|
235
236
|
payload=lambda: _B64_TEXT,
|
|
236
|
-
accel=
|
|
237
|
+
accel=b64_decode,
|
|
237
238
|
base=base64.b64decode,
|
|
238
239
|
)
|
|
239
240
|
|
|
@@ -429,18 +430,18 @@ case(
|
|
|
429
430
|
# ---------------------------------------------------------------- ids
|
|
430
431
|
|
|
431
432
|
# The twin is blake2b, not sha256. BLAKE2 is what hashlib ships from the BLAKE
|
|
432
|
-
# family, so it is the thing `
|
|
433
|
+
# family, so it is the thing `digest.raw` actually stands in for — and a fair fight.
|
|
433
434
|
# sha256 is *not* one: every ARMv8 and modern x86 core implements it in silicon,
|
|
434
435
|
# so `hashlib.sha256` here is a hardware instruction and beats single-threaded
|
|
435
436
|
# BLAKE3 (350 µs vs 453 µs on an M-series laptop). That is a fact about the chip,
|
|
436
437
|
# not about the algorithm, and benchmarking against it would report a loss the
|
|
437
438
|
# library cannot fix and a caller cannot act on.
|
|
438
439
|
case(
|
|
439
|
-
name="
|
|
440
|
+
name="digest.raw · 1 MiB",
|
|
440
441
|
group="ids",
|
|
441
442
|
backend="blake3",
|
|
442
443
|
payload=lambda: _HASH_BLOB,
|
|
443
|
-
accel=
|
|
444
|
+
accel=digest_raw,
|
|
444
445
|
base=lambda p: hashlib.blake2b(p).digest(),
|
|
445
446
|
note="BLAKE3 vs hashlib's BLAKE2b — the stdlib's own BLAKE-family digest",
|
|
446
447
|
equivalent=False,
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "unstd"
|
|
7
|
-
version = "
|
|
7
|
+
version = "2.0.0"
|
|
8
8
|
description = "stdlib-faithful accelerated replacements behind optional extras"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.12"
|
|
@@ -49,7 +49,6 @@ serde = [
|
|
|
49
49
|
"pybase64>=1.5",
|
|
50
50
|
]
|
|
51
51
|
ids = [
|
|
52
|
-
"blake3>=1.0",
|
|
53
52
|
"uuid-utils>=0.17",
|
|
54
53
|
]
|
|
55
54
|
crypto = [
|
|
@@ -69,7 +68,7 @@ model = [
|
|
|
69
68
|
"pydantic-settings>=2.15",
|
|
70
69
|
]
|
|
71
70
|
rex = [
|
|
72
|
-
"irregex>=2.
|
|
71
|
+
"irregex>=2.6.0",
|
|
73
72
|
]
|
|
74
73
|
containers = [
|
|
75
74
|
"sortedcontainers>=2.4",
|
|
@@ -13,15 +13,17 @@ failing loud where no faithful substitute exists:
|
|
|
13
13
|
- ``serde`` (``json`` / ``base64``) — ``jsonx`` (orjson) · ``b64`` (pybase64) ·
|
|
14
14
|
``ndjson`` (newline-delimited, rides ``jsonx``) · ``structs`` (msgspec, no
|
|
15
15
|
stdlib equivalent). Behind the ``serde`` extra; the rest fall back to stdlib.
|
|
16
|
-
- ``ids`` (``
|
|
17
|
-
|
|
18
|
-
- ``crypto``
|
|
16
|
+
- ``ids`` (``uuid``) — ``uid``: UUIDv7 minting and v5 derivation, falling back to
|
|
17
|
+
a stdlib generator. Behind the ``ids`` extra.
|
|
18
|
+
- ``crypto`` (``hashlib`` / ``hmac`` / ``ssl``) — BLAKE3 digests (content
|
|
19
|
+
hashes and short ids included), key derivation, keyed MACs and the bearer
|
|
19
20
|
envelope, constant-time compare, and a verifying TLS client policy. No
|
|
20
21
|
fallback: ``hashlib`` ships BLAKE2, which is a different algorithm, not a
|
|
21
22
|
slower one.
|
|
22
|
-
- ``time`` — ``timeutil`` (
|
|
23
|
-
protobuf ``Timestamp`` bridge is behind the
|
|
24
|
-
|
|
23
|
+
- ``time`` — ``timeutil`` (clocks, RFC-3339 formatting, ISO parsing, instant
|
|
24
|
+
arithmetic — pure stdlib; its protobuf ``Timestamp`` bridge is behind the
|
|
25
|
+
``proto`` extra) · ``zoned`` (DST-correct wall-clock time over ``whenever``,
|
|
26
|
+
behind the ``time`` extra).
|
|
25
27
|
- ``model`` — the strict Pydantic wire-shape spine: ``StrictModel`` ·
|
|
26
28
|
``FrozenModel`` · ``StrictSettings``. Behind the ``model`` extra (pydantic has
|
|
27
29
|
no stdlib equivalent, so there is no fallback).
|
|
@@ -33,7 +35,7 @@ failing loud where no faithful substitute exists:
|
|
|
33
35
|
- ``rand`` (``random``) — numpy PCG64 bulk draws; security-sensitive draws stay
|
|
34
36
|
on ``secrets`` regardless of the extra.
|
|
35
37
|
- ``pack`` (``struct``) — numpy bulk pack/unpack.
|
|
36
|
-
- ``clone`` (``copy``) —
|
|
38
|
+
- ``clone`` (``copy``) — an exact-type structural deep clone, pure stdlib.
|
|
37
39
|
- ``text`` (``difflib``) — rapidfuzz similarity and fuzzy matching.
|
|
38
40
|
- ``audio`` (``wave``) — soundfile read/write plus a streaming writer.
|
|
39
41
|
- ``toml`` (``tomllib``) — reading is stdlib; writing needs tomlkit.
|
|
@@ -43,8 +45,9 @@ failing loud where no faithful substitute exists:
|
|
|
43
45
|
Import the module, not its members::
|
|
44
46
|
|
|
45
47
|
from unstd.serde import jsonx, b64, structs, ndjson
|
|
46
|
-
from unstd.ids import
|
|
47
|
-
from unstd.
|
|
48
|
+
from unstd.ids import uid
|
|
49
|
+
from unstd.crypto import digest, token
|
|
50
|
+
from unstd.time import timeutil, zoned
|
|
48
51
|
from unstd.model import StrictModel, FrozenModel, StrictSettings
|
|
49
52
|
"""
|
|
50
53
|
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""The deferred-raise placeholder every capability without a stdlib fallback binds.
|
|
2
|
+
|
|
3
|
+
Where a backend has no faithful substitute (a sorted container, a persistent
|
|
4
|
+
trie, a TOML writer), its public name still has to exist so the module imports
|
|
5
|
+
cleanly on a base install. :func:`missing` is that name: calling it raises an
|
|
6
|
+
``ImportError`` naming the extra that fixes it, so a consumer that never touches
|
|
7
|
+
the capability pays nothing, and one that does gets a one-line fix.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from typing import TYPE_CHECKING, Never
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from collections.abc import Callable
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def missing(qualname: str, extra: str) -> Callable[..., Never]:
|
|
20
|
+
"""A stand-in for ``unstd.<qualname>`` that raises naming the ``extra`` it needs."""
|
|
21
|
+
msg = f"unstd.{qualname} requires the {extra!r} extra — install with: pip install 'unstd[{extra}]'"
|
|
22
|
+
|
|
23
|
+
def _raise(*_args: object, **_kwargs: object) -> Never:
|
|
24
|
+
raise ImportError(msg)
|
|
25
|
+
|
|
26
|
+
_raise.__name__ = _raise.__qualname__ = qualname.rpartition(".")[2]
|
|
27
|
+
_raise.__doc__ = f"Unavailable — {msg}."
|
|
28
|
+
return _raise
|
|
@@ -11,20 +11,20 @@ writer.
|
|
|
11
11
|
|
|
12
12
|
| File | Role |
|
|
13
13
|
| --------- | ----------------------------------------------------------------------------------------------------------------- |
|
|
14
|
-
| `wavx.py` | `
|
|
14
|
+
| `wavx.py` | `read`/`write` (whole-file) · `blocks`/`Writer` (streaming) · `wrap_pcm` — numpy-native WAV samples ↔ file. |
|
|
15
15
|
|
|
16
16
|
```python
|
|
17
17
|
from unstd.audio import wavx
|
|
18
18
|
|
|
19
|
-
samples, sr = wavx.
|
|
20
|
-
wavx.
|
|
21
|
-
ints, sr = wavx.
|
|
19
|
+
samples, sr = wavx.read("in.wav") # float32 in [-1, 1], sr = Hz
|
|
20
|
+
wavx.write("out.wav", samples, sr) # PCM_16 by default
|
|
21
|
+
ints, sr = wavx.read("in.wav", dtype="int16") # raw signed 16-bit
|
|
22
22
|
|
|
23
23
|
# streaming — for a live TTS/STT pipeline, not a file already fully in memory:
|
|
24
|
-
with wavx.
|
|
24
|
+
with wavx.Writer("out.wav", sr, channels=1) as w:
|
|
25
25
|
for chunk in synth_chunks:
|
|
26
26
|
w.write(chunk)
|
|
27
|
-
for block in wavx.
|
|
27
|
+
for block in wavx.blocks("in.wav", blocksize=4096):
|
|
28
28
|
play(block)
|
|
29
29
|
|
|
30
30
|
wavx.wrap_pcm(raw_pcm_bytes, sr, channels=1, width=2) # header only, no decode
|
|
@@ -55,13 +55,13 @@ back consistently on another: a _w_-bit sample ↔ float via `± 2**(w-1)`, so
|
|
|
55
55
|
`float32 → PCM16 → float32` round-trips lossless to within one quantization step
|
|
56
56
|
(`1/32768`).
|
|
57
57
|
|
|
58
|
-
## Streaming — `
|
|
58
|
+
## Streaming — `Writer` / `blocks`
|
|
59
59
|
|
|
60
|
-
`
|
|
61
|
-
synthesis, STT playback) wants frames as they arrive instead. `
|
|
60
|
+
`read`/`write` buffer the whole file; a live pipeline (streaming TTS
|
|
61
|
+
synthesis, STT playback) wants frames as they arrive instead. `Writer` is a
|
|
62
62
|
context manager — `channels`/`subtype` are fixed up front (a stream has no
|
|
63
63
|
complete array to infer them from), and each `.write(chunk)` call appends one
|
|
64
|
-
chunk; `
|
|
64
|
+
chunk; `blocks(path, blocksize)` is the read-side generator, `blocksize`
|
|
65
65
|
frames at a time. Fast path: `soundfile`'s own `SoundFile`/`blocks` streaming
|
|
66
66
|
API. Fallback: stdlib `wave`'s `writeframesraw`/chunked `readframes`, which
|
|
67
67
|
requires the write target be **seekable** (a real file path, or a file object
|
|
@@ -74,7 +74,7 @@ Some PCM never becomes samples: bytes straight off a wire (a codec's output, a
|
|
|
74
74
|
socket frame, a transcription upload) just need a RIFF/WAVE container around
|
|
75
75
|
them. `wrap_pcm(raw, samplerate, channels=1, width=2)` does exactly that — no
|
|
76
76
|
scale, no clamp, no requantize — where routing the same bytes through
|
|
77
|
-
`
|
|
77
|
+
`write` would decode to an array and re-quantize back, paying two
|
|
78
78
|
conversions and a quantization step to reproduce its own input.
|
|
79
79
|
|
|
80
80
|
## Prior art
|