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.
Files changed (121) hide show
  1. {unstd-1.1.0 → unstd-2.0.0}/CHANGELOG.md +169 -0
  2. {unstd-1.1.0 → unstd-2.0.0}/PKG-INFO +4 -5
  3. {unstd-1.1.0 → unstd-2.0.0}/README.md +2 -2
  4. {unstd-1.1.0 → unstd-2.0.0}/bench/README.md +2 -2
  5. {unstd-1.1.0 → unstd-2.0.0}/bench/baseline.json +3 -3
  6. {unstd-1.1.0 → unstd-2.0.0}/bench/cases.py +10 -9
  7. {unstd-1.1.0 → unstd-2.0.0}/pyproject.toml +2 -3
  8. {unstd-1.1.0 → unstd-2.0.0}/src/unstd/__init__.py +12 -9
  9. unstd-2.0.0/src/unstd/_extra.py +28 -0
  10. {unstd-1.1.0 → unstd-2.0.0}/src/unstd/audio/README.md +11 -11
  11. {unstd-1.1.0 → unstd-2.0.0}/src/unstd/audio/wavx.py +85 -53
  12. {unstd-1.1.0 → unstd-2.0.0}/src/unstd/clone/structural.py +6 -1
  13. {unstd-1.1.0 → unstd-2.0.0}/src/unstd/containers/persistent.py +4 -4
  14. {unstd-1.1.0 → unstd-2.0.0}/src/unstd/containers/sorted.py +6 -5
  15. {unstd-1.1.0 → unstd-2.0.0}/src/unstd/crypto/README.md +17 -14
  16. unstd-2.0.0/src/unstd/crypto/__init__.py +20 -0
  17. unstd-2.0.0/src/unstd/crypto/_blake3.py +19 -0
  18. {unstd-1.1.0 → unstd-2.0.0}/src/unstd/crypto/digest.py +53 -80
  19. {unstd-1.1.0 → unstd-2.0.0}/src/unstd/crypto/tls.py +11 -6
  20. {unstd-1.1.0 → unstd-2.0.0}/src/unstd/crypto/token.py +46 -51
  21. {unstd-1.1.0 → unstd-2.0.0}/src/unstd/fs/README.md +5 -3
  22. {unstd-1.1.0 → unstd-2.0.0}/src/unstd/fs/__init__.py +44 -47
  23. {unstd-1.1.0 → unstd-2.0.0}/src/unstd/ids/README.md +6 -9
  24. {unstd-1.1.0 → unstd-2.0.0}/src/unstd/ids/__init__.py +5 -5
  25. {unstd-1.1.0 → unstd-2.0.0}/src/unstd/ids/uid.py +43 -41
  26. {unstd-1.1.0 → unstd-2.0.0}/src/unstd/iters/README.md +6 -6
  27. {unstd-1.1.0 → unstd-2.0.0}/src/unstd/iters/__init__.py +2 -2
  28. unstd-1.1.0/src/unstd/iters/runs.py → unstd-2.0.0/src/unstd/iters/consecutive.py +17 -8
  29. {unstd-1.1.0 → unstd-2.0.0}/src/unstd/iters/distinct.py +24 -10
  30. unstd-2.0.0/src/unstd/iters/reshape.py +52 -0
  31. {unstd-1.1.0 → unstd-2.0.0}/src/unstd/iters/select.py +10 -2
  32. {unstd-1.1.0 → unstd-2.0.0}/src/unstd/pack/binpack.py +5 -4
  33. {unstd-1.1.0 → unstd-2.0.0}/src/unstd/proc/README.md +4 -3
  34. {unstd-1.1.0 → unstd-2.0.0}/src/unstd/proc/__init__.py +6 -8
  35. {unstd-1.1.0 → unstd-2.0.0}/src/unstd/rand/__init__.py +6 -6
  36. {unstd-1.1.0 → unstd-2.0.0}/src/unstd/rand/crypto.py +12 -28
  37. {unstd-1.1.0 → unstd-2.0.0}/src/unstd/rand/draw.py +36 -57
  38. {unstd-1.1.0 → unstd-2.0.0}/src/unstd/rex/README.md +17 -12
  39. {unstd-1.1.0 → unstd-2.0.0}/src/unstd/rex/__init__.py +101 -80
  40. {unstd-1.1.0 → unstd-2.0.0}/src/unstd/serde/README.md +14 -11
  41. unstd-2.0.0/src/unstd/serde/b64.py +127 -0
  42. {unstd-1.1.0 → unstd-2.0.0}/src/unstd/serde/jsonx.py +61 -44
  43. {unstd-1.1.0 → unstd-2.0.0}/src/unstd/serde/ndjson.py +50 -22
  44. {unstd-1.1.0 → unstd-2.0.0}/src/unstd/serde/structs.py +19 -40
  45. {unstd-1.1.0 → unstd-2.0.0}/src/unstd/text/fuzz.py +141 -117
  46. unstd-2.0.0/src/unstd/time/README.md +69 -0
  47. unstd-2.0.0/src/unstd/time/__init__.py +15 -0
  48. unstd-2.0.0/src/unstd/time/timeutil.py +292 -0
  49. unstd-2.0.0/src/unstd/time/zoned.py +133 -0
  50. {unstd-1.1.0 → unstd-2.0.0}/src/unstd/toml/README.md +1 -1
  51. unstd-2.0.0/src/unstd/toml/__init__.py +129 -0
  52. {unstd-1.1.0 → unstd-2.0.0}/tests/audio/test_audio.py +74 -49
  53. {unstd-1.1.0 → unstd-2.0.0}/tests/conftest.py +3 -3
  54. {unstd-1.1.0 → unstd-2.0.0}/tests/containers/test_containers.py +3 -3
  55. {unstd-1.1.0 → unstd-2.0.0}/tests/crypto/test_adverse.py +51 -41
  56. {unstd-1.1.0 → unstd-2.0.0}/tests/crypto/test_vectors.py +15 -15
  57. {unstd-1.1.0 → unstd-2.0.0}/tests/fs/test_fs.py +28 -26
  58. {unstd-1.1.0 → unstd-2.0.0}/tests/iters/test_iters.py +79 -12
  59. {unstd-1.1.0 → unstd-2.0.0}/tests/proc/test_proc.py +8 -5
  60. {unstd-1.1.0 → unstd-2.0.0}/tests/properties/test_toml_invariants.py +13 -6
  61. {unstd-1.1.0 → unstd-2.0.0}/tests/properties/test_wav_quant.py +2 -2
  62. {unstd-1.1.0 → unstd-2.0.0}/tests/rex/test_rex.py +102 -0
  63. unstd-2.0.0/tests/serde/test_b64.py +135 -0
  64. {unstd-1.1.0 → unstd-2.0.0}/tests/serde/test_ndjson.py +56 -18
  65. {unstd-1.1.0 → unstd-2.0.0}/tests/serde/test_structs.py +19 -19
  66. {unstd-1.1.0 → unstd-2.0.0}/tests/test_package.py +1 -1
  67. {unstd-1.1.0 → unstd-2.0.0}/tests/text/test_text.py +42 -1
  68. {unstd-1.1.0 → unstd-2.0.0}/tests/time/test_timeutil.py +170 -47
  69. unstd-2.0.0/tests/time/test_zoned.py +172 -0
  70. {unstd-1.1.0 → unstd-2.0.0}/tests/toml/test_toml.py +36 -18
  71. {unstd-1.1.0 → unstd-2.0.0}/uv.lock +10 -12
  72. unstd-1.1.0/src/unstd/containers/guard.py +0 -48
  73. unstd-1.1.0/src/unstd/crypto/__init__.py +0 -34
  74. unstd-1.1.0/src/unstd/ids/hash.py +0 -39
  75. unstd-1.1.0/src/unstd/iters/reshape.py +0 -56
  76. unstd-1.1.0/src/unstd/serde/b64.py +0 -142
  77. unstd-1.1.0/src/unstd/time/README.md +0 -47
  78. unstd-1.1.0/src/unstd/time/__init__.py +0 -14
  79. unstd-1.1.0/src/unstd/time/dateutil.py +0 -194
  80. unstd-1.1.0/src/unstd/time/timeutil.py +0 -219
  81. unstd-1.1.0/src/unstd/toml/__init__.py +0 -201
  82. unstd-1.1.0/tests/serde/test_b64.py +0 -117
  83. unstd-1.1.0/tests/time/test_dateutil.py +0 -305
  84. {unstd-1.1.0 → unstd-2.0.0}/.github/workflows/ci.yml +0 -0
  85. {unstd-1.1.0 → unstd-2.0.0}/.github/workflows/release.yml +0 -0
  86. {unstd-1.1.0 → unstd-2.0.0}/.gitignore +0 -0
  87. {unstd-1.1.0 → unstd-2.0.0}/LICENSE +0 -0
  88. {unstd-1.1.0 → unstd-2.0.0}/NOTICE +0 -0
  89. {unstd-1.1.0 → unstd-2.0.0}/bench/__init__.py +0 -0
  90. {unstd-1.1.0 → unstd-2.0.0}/bench/__main__.py +0 -0
  91. {unstd-1.1.0 → unstd-2.0.0}/bench/report.py +0 -0
  92. {unstd-1.1.0 → unstd-2.0.0}/bench/timing.py +0 -0
  93. {unstd-1.1.0 → unstd-2.0.0}/src/unstd/audio/__init__.py +0 -0
  94. {unstd-1.1.0 → unstd-2.0.0}/src/unstd/clone/README.md +0 -0
  95. {unstd-1.1.0 → unstd-2.0.0}/src/unstd/clone/__init__.py +0 -0
  96. {unstd-1.1.0 → unstd-2.0.0}/src/unstd/containers/README.md +0 -0
  97. {unstd-1.1.0 → unstd-2.0.0}/src/unstd/containers/__init__.py +0 -0
  98. {unstd-1.1.0 → unstd-2.0.0}/src/unstd/model/README.md +0 -0
  99. {unstd-1.1.0 → unstd-2.0.0}/src/unstd/model/__init__.py +0 -0
  100. {unstd-1.1.0 → unstd-2.0.0}/src/unstd/model/base.py +0 -0
  101. {unstd-1.1.0 → unstd-2.0.0}/src/unstd/model/settings.py +0 -0
  102. {unstd-1.1.0 → unstd-2.0.0}/src/unstd/pack/README.md +0 -0
  103. {unstd-1.1.0 → unstd-2.0.0}/src/unstd/pack/__init__.py +0 -0
  104. {unstd-1.1.0 → unstd-2.0.0}/src/unstd/py.typed +0 -0
  105. {unstd-1.1.0 → unstd-2.0.0}/src/unstd/rand/README.md +0 -0
  106. {unstd-1.1.0 → unstd-2.0.0}/src/unstd/serde/__init__.py +0 -0
  107. {unstd-1.1.0 → unstd-2.0.0}/src/unstd/text/README.md +0 -0
  108. {unstd-1.1.0 → unstd-2.0.0}/src/unstd/text/__init__.py +0 -0
  109. {unstd-1.1.0 → unstd-2.0.0}/tests/clone/test_clone.py +0 -0
  110. {unstd-1.1.0 → unstd-2.0.0}/tests/crypto/blake3_reference_vectors.json +0 -0
  111. {unstd-1.1.0 → unstd-2.0.0}/tests/ids/test_uid.py +0 -0
  112. {unstd-1.1.0 → unstd-2.0.0}/tests/model/test_model.py +0 -0
  113. {unstd-1.1.0 → unstd-2.0.0}/tests/pack/test_pack.py +0 -0
  114. {unstd-1.1.0 → unstd-2.0.0}/tests/properties/README.md +0 -0
  115. {unstd-1.1.0 → unstd-2.0.0}/tests/properties/test_clone_invariants.py +0 -0
  116. {unstd-1.1.0 → unstd-2.0.0}/tests/properties/test_pack_invariants.py +0 -0
  117. {unstd-1.1.0 → unstd-2.0.0}/tests/properties/test_serde_parity.py +0 -0
  118. {unstd-1.1.0 → unstd-2.0.0}/tests/properties/test_uuid7.py +0 -0
  119. {unstd-1.1.0 → unstd-2.0.0}/tests/rand/test_rand.py +0 -0
  120. {unstd-1.1.0 → unstd-2.0.0}/tests/serde/test_jsonx.py +0 -0
  121. {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: 1.1.0
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.3.0; extra == 'rex'
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]` | blake3 / uuid-utils → `hash` requires the extra; `uid` falls back to stdlib UUIDv7 |
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; `dateutil` requires `whenever`, and `timeutil`'s protobuf `Timestamp` bridge requires `unstd[proto]` |
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]` | blake3 / uuid-utils → `hash` requires the extra; `uid` falls back to stdlib UUIDv7 |
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; `dateutil` requires `whenever`, and `timeutil`'s protobuf `Timestamp` bridge requires `unstd[proto]` |
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
- - **`hash.sum_`'s honest twin is `blake2b`, not `sha256`.** BLAKE2 is what
96
- `hashlib` ships from the BLAKE family, so it is the thing `sum_` stands in for,
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.b64d · 128 KiB": {
6
+ "b64.decode · 128 KiB": {
7
7
  "floor": 6.42,
8
8
  "measured": 8.94,
9
9
  "group": "serde"
10
10
  },
11
- "b64.b64s · 128 KiB": {
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
- "hash.sum_ · 1 MiB": {
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.ids.hash import sum_
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 b64d, b64s
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.b64s · 128 KiB",
224
+ name="b64.encode · 128 KiB",
224
225
  group="serde",
225
226
  backend="pybase64",
226
227
  payload=lambda: _BLOB,
227
- accel=b64s,
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.b64d · 128 KiB",
233
+ name="b64.decode · 128 KiB",
233
234
  group="serde",
234
235
  backend="pybase64",
235
236
  payload=lambda: _B64_TEXT,
236
- accel=b64d,
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 `sum_` actually stands in for — and a fair fight.
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="hash.sum_ · 1 MiB",
440
+ name="digest.raw · 1 MiB",
440
441
  group="ids",
441
442
  backend="blake3",
442
443
  payload=lambda: _HASH_BLOB,
443
- accel=sum_,
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 = "1.1.0"
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.3.0",
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`` (``hashlib`` / ``uuid``) — ``hash`` (BLAKE3, fails loud) · ``uid``
17
- (UUIDv7, falls back to a stdlib generator). Behind the ``ids`` extra.
18
- - ``crypto`` — BLAKE3 digests, key derivation, keyed MACs and the bearer
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`` (pure-stdlib clock seam, always available; its
23
- protobuf ``Timestamp`` bridge is behind the ``proto`` extra) · ``dateutil``
24
- (the ``whenever`` typed-instant seam, behind the ``time`` extra).
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``) — msgspec round-trip, falls back to ``copy.deepcopy``.
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 hash, uid
47
- from unstd.time import timeutil, dateutil
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` | `read_wav`/`write_wav` (whole-file) · `read_blocks`/`WavWriter` (streaming) · `wrap_pcm` — numpy-native WAV samples ↔ file. |
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.read_wav("in.wav") # float32 in [-1, 1], sr = Hz
20
- wavx.write_wav("out.wav", samples, sr) # PCM_16 by default
21
- ints, sr = wavx.read_wav("in.wav", dtype="int16") # raw signed 16-bit
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.WavWriter("out.wav", sr, channels=1) as w:
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.read_blocks("in.wav", blocksize=4096):
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 — `WavWriter` / `read_blocks`
58
+ ## Streaming — `Writer` / `blocks`
59
59
 
60
- `read_wav`/`write_wav` buffer the whole file; a live pipeline (streaming TTS
61
- synthesis, STT playback) wants frames as they arrive instead. `WavWriter` is a
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; `read_blocks(path, blocksize)` is the read-side generator, `blocksize`
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
- `write_wav` would decode to an array and re-quantize back, paying two
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