unstd 1.0.5__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 (126) hide show
  1. unstd-2.0.0/CHANGELOG.md +385 -0
  2. {unstd-1.0.5 → unstd-2.0.0}/PKG-INFO +6 -8
  3. {unstd-1.0.5 → unstd-2.0.0}/README.md +4 -4
  4. {unstd-1.0.5 → unstd-2.0.0}/bench/README.md +11 -2
  5. {unstd-1.0.5 → unstd-2.0.0}/bench/baseline.json +20 -10
  6. {unstd-1.0.5 → unstd-2.0.0}/bench/cases.py +87 -16
  7. {unstd-1.0.5 → unstd-2.0.0}/bench/report.py +10 -2
  8. {unstd-1.0.5 → unstd-2.0.0}/pyproject.toml +6 -6
  9. {unstd-1.0.5 → unstd-2.0.0}/src/unstd/__init__.py +12 -9
  10. unstd-2.0.0/src/unstd/_extra.py +28 -0
  11. {unstd-1.0.5 → unstd-2.0.0}/src/unstd/audio/README.md +11 -11
  12. {unstd-1.0.5 → unstd-2.0.0}/src/unstd/audio/wavx.py +85 -53
  13. unstd-2.0.0/src/unstd/clone/README.md +91 -0
  14. unstd-2.0.0/src/unstd/clone/__init__.py +27 -0
  15. unstd-2.0.0/src/unstd/clone/structural.py +277 -0
  16. {unstd-1.0.5 → unstd-2.0.0}/src/unstd/containers/persistent.py +4 -4
  17. {unstd-1.0.5 → unstd-2.0.0}/src/unstd/containers/sorted.py +6 -5
  18. {unstd-1.0.5 → unstd-2.0.0}/src/unstd/crypto/README.md +17 -14
  19. unstd-2.0.0/src/unstd/crypto/__init__.py +20 -0
  20. unstd-2.0.0/src/unstd/crypto/_blake3.py +19 -0
  21. {unstd-1.0.5 → unstd-2.0.0}/src/unstd/crypto/digest.py +53 -80
  22. {unstd-1.0.5 → unstd-2.0.0}/src/unstd/crypto/tls.py +11 -6
  23. {unstd-1.0.5 → unstd-2.0.0}/src/unstd/crypto/token.py +46 -51
  24. {unstd-1.0.5 → unstd-2.0.0}/src/unstd/fs/README.md +5 -3
  25. {unstd-1.0.5 → unstd-2.0.0}/src/unstd/fs/__init__.py +44 -47
  26. {unstd-1.0.5 → unstd-2.0.0}/src/unstd/ids/README.md +6 -9
  27. {unstd-1.0.5 → unstd-2.0.0}/src/unstd/ids/__init__.py +5 -5
  28. {unstd-1.0.5 → unstd-2.0.0}/src/unstd/ids/uid.py +43 -41
  29. {unstd-1.0.5 → unstd-2.0.0}/src/unstd/iters/README.md +6 -6
  30. {unstd-1.0.5 → unstd-2.0.0}/src/unstd/iters/__init__.py +2 -2
  31. unstd-1.0.5/src/unstd/iters/runs.py → unstd-2.0.0/src/unstd/iters/consecutive.py +17 -8
  32. {unstd-1.0.5 → unstd-2.0.0}/src/unstd/iters/distinct.py +24 -10
  33. unstd-2.0.0/src/unstd/iters/reshape.py +52 -0
  34. {unstd-1.0.5 → unstd-2.0.0}/src/unstd/iters/select.py +10 -2
  35. {unstd-1.0.5 → unstd-2.0.0}/src/unstd/pack/binpack.py +5 -4
  36. {unstd-1.0.5 → unstd-2.0.0}/src/unstd/proc/README.md +4 -3
  37. {unstd-1.0.5 → unstd-2.0.0}/src/unstd/proc/__init__.py +6 -8
  38. {unstd-1.0.5 → unstd-2.0.0}/src/unstd/rand/__init__.py +6 -6
  39. {unstd-1.0.5 → unstd-2.0.0}/src/unstd/rand/crypto.py +12 -28
  40. {unstd-1.0.5 → unstd-2.0.0}/src/unstd/rand/draw.py +36 -57
  41. {unstd-1.0.5 → unstd-2.0.0}/src/unstd/rex/README.md +17 -12
  42. {unstd-1.0.5 → unstd-2.0.0}/src/unstd/rex/__init__.py +101 -80
  43. {unstd-1.0.5 → unstd-2.0.0}/src/unstd/serde/README.md +14 -11
  44. unstd-2.0.0/src/unstd/serde/b64.py +127 -0
  45. {unstd-1.0.5 → unstd-2.0.0}/src/unstd/serde/jsonx.py +61 -44
  46. {unstd-1.0.5 → unstd-2.0.0}/src/unstd/serde/ndjson.py +50 -22
  47. {unstd-1.0.5 → unstd-2.0.0}/src/unstd/serde/structs.py +19 -40
  48. {unstd-1.0.5 → unstd-2.0.0}/src/unstd/text/fuzz.py +141 -117
  49. unstd-2.0.0/src/unstd/time/README.md +69 -0
  50. unstd-2.0.0/src/unstd/time/__init__.py +15 -0
  51. unstd-2.0.0/src/unstd/time/timeutil.py +292 -0
  52. unstd-2.0.0/src/unstd/time/zoned.py +133 -0
  53. {unstd-1.0.5 → unstd-2.0.0}/src/unstd/toml/README.md +1 -1
  54. unstd-2.0.0/src/unstd/toml/__init__.py +129 -0
  55. {unstd-1.0.5 → unstd-2.0.0}/tests/audio/test_audio.py +74 -49
  56. unstd-2.0.0/tests/clone/test_clone.py +386 -0
  57. {unstd-1.0.5 → unstd-2.0.0}/tests/conftest.py +3 -3
  58. {unstd-1.0.5 → unstd-2.0.0}/tests/containers/test_containers.py +3 -3
  59. {unstd-1.0.5 → unstd-2.0.0}/tests/crypto/test_adverse.py +51 -41
  60. {unstd-1.0.5 → unstd-2.0.0}/tests/crypto/test_vectors.py +15 -15
  61. {unstd-1.0.5 → unstd-2.0.0}/tests/fs/test_fs.py +28 -26
  62. {unstd-1.0.5 → unstd-2.0.0}/tests/iters/test_iters.py +79 -12
  63. {unstd-1.0.5 → unstd-2.0.0}/tests/proc/test_proc.py +8 -5
  64. {unstd-1.0.5 → unstd-2.0.0}/tests/properties/test_clone_invariants.py +21 -14
  65. {unstd-1.0.5 → unstd-2.0.0}/tests/properties/test_toml_invariants.py +13 -6
  66. {unstd-1.0.5 → unstd-2.0.0}/tests/properties/test_wav_quant.py +2 -2
  67. {unstd-1.0.5 → unstd-2.0.0}/tests/rex/test_rex.py +102 -0
  68. unstd-2.0.0/tests/serde/test_b64.py +135 -0
  69. {unstd-1.0.5 → unstd-2.0.0}/tests/serde/test_ndjson.py +56 -18
  70. {unstd-1.0.5 → unstd-2.0.0}/tests/serde/test_structs.py +19 -19
  71. {unstd-1.0.5 → unstd-2.0.0}/tests/test_bench.py +16 -0
  72. {unstd-1.0.5 → unstd-2.0.0}/tests/test_package.py +1 -1
  73. {unstd-1.0.5 → unstd-2.0.0}/tests/text/test_text.py +42 -1
  74. {unstd-1.0.5 → unstd-2.0.0}/tests/time/test_timeutil.py +170 -47
  75. unstd-2.0.0/tests/time/test_zoned.py +172 -0
  76. {unstd-1.0.5 → unstd-2.0.0}/tests/toml/test_toml.py +36 -18
  77. {unstd-1.0.5 → unstd-2.0.0}/uv.lock +10 -16
  78. unstd-1.0.5/CHANGELOG.md +0 -184
  79. unstd-1.0.5/src/unstd/clone/README.md +0 -90
  80. unstd-1.0.5/src/unstd/clone/__init__.py +0 -32
  81. unstd-1.0.5/src/unstd/clone/structural.py +0 -126
  82. unstd-1.0.5/src/unstd/containers/guard.py +0 -48
  83. unstd-1.0.5/src/unstd/crypto/__init__.py +0 -34
  84. unstd-1.0.5/src/unstd/ids/hash.py +0 -39
  85. unstd-1.0.5/src/unstd/iters/reshape.py +0 -56
  86. unstd-1.0.5/src/unstd/serde/b64.py +0 -142
  87. unstd-1.0.5/src/unstd/time/README.md +0 -47
  88. unstd-1.0.5/src/unstd/time/__init__.py +0 -14
  89. unstd-1.0.5/src/unstd/time/dateutil.py +0 -194
  90. unstd-1.0.5/src/unstd/time/timeutil.py +0 -219
  91. unstd-1.0.5/src/unstd/toml/__init__.py +0 -201
  92. unstd-1.0.5/tests/clone/test_clone.py +0 -230
  93. unstd-1.0.5/tests/serde/test_b64.py +0 -117
  94. unstd-1.0.5/tests/time/test_dateutil.py +0 -305
  95. {unstd-1.0.5 → unstd-2.0.0}/.github/workflows/ci.yml +0 -0
  96. {unstd-1.0.5 → unstd-2.0.0}/.github/workflows/release.yml +0 -0
  97. {unstd-1.0.5 → unstd-2.0.0}/.gitignore +0 -0
  98. {unstd-1.0.5 → unstd-2.0.0}/LICENSE +0 -0
  99. {unstd-1.0.5 → unstd-2.0.0}/NOTICE +0 -0
  100. {unstd-1.0.5 → unstd-2.0.0}/bench/__init__.py +0 -0
  101. {unstd-1.0.5 → unstd-2.0.0}/bench/__main__.py +0 -0
  102. {unstd-1.0.5 → unstd-2.0.0}/bench/timing.py +0 -0
  103. {unstd-1.0.5 → unstd-2.0.0}/src/unstd/audio/__init__.py +0 -0
  104. {unstd-1.0.5 → unstd-2.0.0}/src/unstd/containers/README.md +0 -0
  105. {unstd-1.0.5 → unstd-2.0.0}/src/unstd/containers/__init__.py +0 -0
  106. {unstd-1.0.5 → unstd-2.0.0}/src/unstd/model/README.md +0 -0
  107. {unstd-1.0.5 → unstd-2.0.0}/src/unstd/model/__init__.py +0 -0
  108. {unstd-1.0.5 → unstd-2.0.0}/src/unstd/model/base.py +0 -0
  109. {unstd-1.0.5 → unstd-2.0.0}/src/unstd/model/settings.py +0 -0
  110. {unstd-1.0.5 → unstd-2.0.0}/src/unstd/pack/README.md +0 -0
  111. {unstd-1.0.5 → unstd-2.0.0}/src/unstd/pack/__init__.py +0 -0
  112. {unstd-1.0.5 → unstd-2.0.0}/src/unstd/py.typed +0 -0
  113. {unstd-1.0.5 → unstd-2.0.0}/src/unstd/rand/README.md +0 -0
  114. {unstd-1.0.5 → unstd-2.0.0}/src/unstd/serde/__init__.py +0 -0
  115. {unstd-1.0.5 → unstd-2.0.0}/src/unstd/text/README.md +0 -0
  116. {unstd-1.0.5 → unstd-2.0.0}/src/unstd/text/__init__.py +0 -0
  117. {unstd-1.0.5 → unstd-2.0.0}/tests/crypto/blake3_reference_vectors.json +0 -0
  118. {unstd-1.0.5 → unstd-2.0.0}/tests/ids/test_uid.py +0 -0
  119. {unstd-1.0.5 → unstd-2.0.0}/tests/model/test_model.py +0 -0
  120. {unstd-1.0.5 → unstd-2.0.0}/tests/pack/test_pack.py +0 -0
  121. {unstd-1.0.5 → unstd-2.0.0}/tests/properties/README.md +0 -0
  122. {unstd-1.0.5 → unstd-2.0.0}/tests/properties/test_pack_invariants.py +0 -0
  123. {unstd-1.0.5 → unstd-2.0.0}/tests/properties/test_serde_parity.py +0 -0
  124. {unstd-1.0.5 → unstd-2.0.0}/tests/properties/test_uuid7.py +0 -0
  125. {unstd-1.0.5 → unstd-2.0.0}/tests/rand/test_rand.py +0 -0
  126. {unstd-1.0.5 → unstd-2.0.0}/tests/serde/test_jsonx.py +0 -0
@@ -0,0 +1,385 @@
1
+ # Changelog
2
+
3
+ <!-- towncrier release notes start -->
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
+
174
+ ## [1.1.0] - 2026-09-27
175
+
176
+ ### Fixed
177
+
178
+ - `clone.deep` returns the types it was given. Its msgspec round-trip decoded a
179
+ nested `StrEnum` as `str`, an `IntEnum` as `int`, and an `OrderedDict` or
180
+ `defaultdict` as a plain `dict` - losing the factory with it - and the equality
181
+ check meant to catch a lossy round-trip passed every one, because those values
182
+ compare equal to what came back. `deep` is now an exact-type walk that never
183
+ leaves Python objects, so there is nothing to lose.
184
+ - `python3 -m bench update --group X` no longer erases every other group's floors.
185
+ A narrowed run now merges into the baseline instead of replacing it.
186
+
187
+ ### Changed
188
+
189
+ - `clone.deep` is pure stdlib and faster where callers actually spend time:
190
+ 4.1x over `copy.deepcopy` on a small dict (was 1.2x, slower than deepcopy on
191
+ CPython 3.14), 2.2x on pydantic models (was a failed encode then deepcopy), and
192
+ 4.4x on large JSON trees (was 5.8x on CPython 3.13 - the price of the fix above;
193
+ on 3.14 the two tie). Dataclasses, pydantic models, and `msgspec.Struct` are
194
+ rebuilt the way `deepcopy` rebuilds them, with the walker standing in for its
195
+ recursion; any other node is handed to `deepcopy` on its own while its siblings
196
+ keep walking. The `clone` extra no longer installs anything and stays declared
197
+ so existing `unstd[clone]` requirements resolve without a warning.
198
+
199
+ ### Added
200
+
201
+ - `clone.asdict`: `dataclasses.asdict`'s result on the same walker, 10x faster on
202
+ a dataclass tree with `datetime` leaves. A custom `dict_factory`, a
203
+ non-dataclass argument, or a namedtuple / container subclass in the tree hands
204
+ the call to the stdlib, so the answer is always the stdlib's.
205
+
206
+ ## [1.0.5] - 2026-09-01
207
+
208
+ ### Changed
209
+
210
+ - Prose and fixtures name generic things rather than one deployment's. The
211
+ NDJSON docstring described the format by pointing at two artifacts nobody
212
+ outside their origin has ever seen, and a crypto fixture put a private service
213
+ in a JWT audience - so the docstring names what NDJSON is actually used for,
214
+ and the fixture says `example` like its sibling suite already did. No code
215
+ paths move.
216
+
217
+ ## [1.0.4] - 2026-08-31
218
+
219
+ ### Fixed
220
+
221
+ - The extra floors sit at the minor series rather than the patch we happened to
222
+ develop against, and CI now proves them. 1.0.3 relaxed `==` to `>=` but left
223
+ each floor on an exact patch, so `unstd[model]` still rejected an application
224
+ one pydantic bugfix behind - a resolver error that reads as the application's
225
+ fault. The API a backend offers changes at the minor, which is where a floor
226
+ belongs.
227
+
228
+ ### Added
229
+
230
+ - A `floors` CI job resolves `--resolution lowest-direct` and runs the whole
231
+ suite against the oldest version every floor admits. A floor is a promise that
232
+ the library works that far back, and nothing was testing it: the rest of the
233
+ matrix resolves the newest of everything, so the floors could have been fiction
234
+ indefinitely and only a downstream with an older pin would have found out.
235
+
236
+ ## [1.0.3] - 2026-08-31
237
+
238
+ ### Fixed
239
+
240
+ - Every extra declares a floor rather than an exact `==` pin. The pins were
241
+ inherited from the monorepo this package was extracted from, where an exact
242
+ pin is correct because a monorepo is the application. Published, they meant an
243
+ application already on `pydantic==2.13.4` could not install `unstd[model]` at
244
+ all - the resolver reports the conflict against the application, not against
245
+ us - and two libraries pinning patches are simply not co-installable. The
246
+ floors are the versions each extra is tested against; a consumer that wants an
247
+ exact version has its own manifest and lockfile to say so in.
248
+
249
+ ## [1.0.2] - 2026-08-31
250
+
251
+ ### Fixed
252
+
253
+ - The package ships `py.typed`. Every surface here is annotated and CI has been
254
+ running `mypy --strict` over all of it, and none of that reached a consumer:
255
+ PEP 561 says to ignore the annotations of a package with no marker, so a
256
+ strict downstream saw `jsonx.dumps` as `Any` and wrote a suppression per call
257
+ site to get its build green.
258
+
259
+ ## [1.0.1] - 2026-08-31
260
+
261
+ ### Added
262
+
263
+ - `tls.client_context` takes a `minimum_version`, defaulting to the same TLS 1.3
264
+ it always asserted, and `tls.LEGACY_MINIMUM_VERSION` names the 1.2 floor for a
265
+ peer not yet shown to negotiate 1.3. Without it, a service that dials one such
266
+ peer had to hand-roll a second context — which is where the parameter this
267
+ module refuses to have gets added. Anything below 1.2 raises instead of
268
+ building a context.
269
+
270
+ ### Fixed
271
+
272
+ - `unstd.__version__` is read from install metadata instead of being a literal
273
+ beside the one in `pyproject.toml`. The two drifted on the first patch release
274
+ and the gate caught its own package's version, so the tag was cut and nothing
275
+ shipped; there is now one place to edit.
276
+
277
+ ## [1.0.0] - 2026-08-31
278
+
279
+ ### Added
280
+
281
+ - First public release of the full library: serde, ids, crypto, time, model,
282
+ rex, containers, rand, pack, clone, text, audio, toml, proc, fs, and iters.
283
+ Base install stays pure stdlib; every accelerated backend is an optional extra.
284
+ - `crypto.tls` — the verifying TLS client context, with no parameter for
285
+ skipping verification, split out of the module that used to hold it. Its mTLS
286
+ and pinned-root tests now actually run: they mint a throwaway self-signed
287
+ certificate, which needs `cryptography`, which nothing had declared, so all
288
+ three had been skipping rather than passing.
289
+ - `digest.legacy_blake2b` — the one deliberate exception to "one digest",
290
+ for an identifier already defined in terms of BLAKE2b that cannot change
291
+ algorithm without renaming every value derived from it. Keeping that case
292
+ inside the seam is what stops it becoming a reason to import `hashlib` next
293
+ door and grow a second digest for new work too.
294
+ - `ruff` and `mypy --strict` now run in CI, and `release.yml` calls the CI
295
+ workflow as a gate rather than keeping its own copy of one — so the bar a tag
296
+ clears is the bar a pull request clears, by construction. Neither tool had ever
297
+ run over this package; the first pass found 26 type errors.
298
+ - A `proto` extra. `timeutil`'s protobuf `Timestamp` bridge imported
299
+ `google.protobuf` with nothing declaring it, so the two `proto_timestamp`
300
+ functions raised `ModuleNotFoundError` on any install that had not happened to
301
+ pick protobuf up sideways. The import is guarded now and names the extra.
302
+ - `bench` — a harness behind the speedup numbers the READMEs print. Each case is
303
+ one accelerated call against the stdlib call it stands in for, reported as a
304
+ ratio, with `bench/baseline.json` pinning a floor per case that `update` may
305
+ raise and never lower. It refuses to grade a missing backend as a pass, and
306
+ refuses to report a ratio between two functions that disagree on the answer.
307
+ - The suite runs on a base install. It could not before: four surfaces have no
308
+ stdlib fallback and raise at import, which pytest reports as a collection
309
+ error, so `pip install unstd && pytest` aborted before running anything — and
310
+ the guarded-fallback paths, which are the reason the library exists, had never
311
+ been exercised by anything. `tests/conftest.py` now decides collectability by
312
+ probing importability, and the fallback CI leg runs the whole suite (477 tests)
313
+ instead of a hand-maintained subset.
314
+ - Tests for `ndjson`, which had none and 0% coverage; for `timeutil`, which had
315
+ none; and for the package's own description of itself — the top-level docstring
316
+ listed four module groups while sixteen shipped, so the prose, the README table
317
+ and the directory listing are now held to each other, along with the
318
+ zero-dependency claim.
319
+
320
+ ### Changed
321
+
322
+ - `crypto` no longer projects an external algorithm table. The widths it
323
+ enforces are BLAKE3's own — 32 bytes of digest, 32 of key, 32 of tag — so they
324
+ live beside the calls they bound (`digest.DIGEST_BYTES` / `KDF_BYTES`,
325
+ `token.KEY_BYTES` / `TAG_BYTES`) instead of being imported from a generated
326
+ registry. `derive_key` takes any context string and says plainly that
327
+ registering one is the caller's job; nothing about the derivation changed.
328
+ - The crypto known-answer vectors are now the BLAKE3 team's own published
329
+ `test_vectors.json`, vendored verbatim and attributed in NOTICE. All three
330
+ modes — plain hash, keyed hash (the MAC), and `derive_key` — are checked
331
+ against the authors' answers across 35 input lengths spanning the chunk and
332
+ subtree boundaries, where the previous fixture carried seven hand-picked rows.
333
+ - `rex` is now backed by [irregex](https://github.com/The-Billy-Company/irregex)
334
+ (`import irgx`) instead of `google-re2`; the extra installs `irregex>=2.3.0`.
335
+ The surface, the stdlib-`re` fidelity contract, and the degrade-to-`re`
336
+ fallback for backreference/lookaround patterns are unchanged.
337
+ - The linear-time guarantee now covers three families that previously degraded to
338
+ backtracking `re`: `re.VERBOSE`, `re.ASCII`, and `bytes` patterns. `bytes`
339
+ patterns compile byte-oriented, so `rb"\w+"` stops at `caf` in `café` exactly
340
+ as stdlib does.
341
+ - Two divergences that were always true of `rex` are now documented and pinned by
342
+ tests rather than left to be discovered: `$` matches the absolute end of the
343
+ text (not the position before a trailing newline, which is Perl's and stdlib's
344
+ rule), and POSIX bracket expressions like `[[:alpha:]]` are a real class to the
345
+ linear engine but a nested-set typo to stdlib.
346
+
347
+ ### Fixed
348
+
349
+ - Nine `rex` signatures annotated their `pattern` and `string` parameters as
350
+ `DOTALL` — the regex flag — instead of `S`, the type parameter they declare.
351
+ Ruff's `FURB167` rewrites `re.S` to `re.DOTALL`, marks the fix safe, and had
352
+ spliced its replacement over every PEP 695 `S` in the file. The rule is now
353
+ disabled here, with the reason written down next to it.
354
+ - `timeutil.human_now_tz` ignored the timezone it was passed. It built the right
355
+ instant and then called `.astimezone()` with no argument, which converts back
356
+ to local time, so every caller got local time under another zone's name.
357
+ - `timeutil.iso_fmt` stamped a literal `Z` on whatever it was handed. A
358
+ `datetime` in any other zone came out labelled UTC while still carrying its
359
+ original wall-clock digits — a silent offset, in the format used for the wire.
360
+ Aware datetimes are converted to UTC first now; naive ones are relabelled.
361
+ - `timeutil`'s human format string used `%-d` and `%-I`, which are a glibc/BSD
362
+ extension. `strftime` raises `ValueError` on both under Windows, so every
363
+ human-facing timestamp in the module was a hard failure on one of the three
364
+ platforms the package claims to support.
365
+ - A `bytearray` or `memoryview` pattern now raises `TypeError`, matching what
366
+ stdlib `re` does with one. The backends accept any buffer as a pattern, so
367
+ `rex` was quietly accepting patterns its own twin refuses — being a superset is
368
+ a divergence too, and code written against it broke the moment the extra was
369
+ absent and the `re` fallback took over.
370
+ - `sub` and `subn` now type a callable `repl` as receiving either backend's match
371
+ object, which is what has always happened at runtime. The two are
372
+ interchangeable for everything a replacement function reads (`group`, `groups`,
373
+ `groupdict`, `span`, `expand`, `start`, `end`); only `.pos` and `.endpos` are
374
+ stdlib-only.
375
+ - `findall` is typed `list[S | tuple[S, ...]]` rather than a union of two list
376
+ types, so the element type survives instead of collapsing (typeshed spells the
377
+ same fact `list[Any]`). Runtime results are unchanged.
378
+
379
+ ## [0.0.1] - 2026-08-31
380
+
381
+ Parked a good name.
382
+
383
+ ### Added
384
+
385
+ - Name reservation on PyPI.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: unstd
3
- Version: 1.0.5
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
@@ -24,14 +24,12 @@ Provides-Extra: audio
24
24
  Requires-Dist: numpy>=2.5; extra == 'audio'
25
25
  Requires-Dist: soundfile>=0.14; extra == 'audio'
26
26
  Provides-Extra: clone
27
- Requires-Dist: msgspec>=0.21; extra == 'clone'
28
27
  Provides-Extra: containers
29
28
  Requires-Dist: immutables>=0.21; extra == 'containers'
30
29
  Requires-Dist: sortedcontainers>=2.4; extra == 'containers'
31
30
  Provides-Extra: crypto
32
31
  Requires-Dist: blake3>=1.0; extra == 'crypto'
33
32
  Provides-Extra: ids
34
- Requires-Dist: blake3>=1.0; extra == 'ids'
35
33
  Requires-Dist: uuid-utils>=0.17; extra == 'ids'
36
34
  Provides-Extra: model
37
35
  Requires-Dist: pydantic-settings>=2.15; extra == 'model'
@@ -43,7 +41,7 @@ Requires-Dist: protobuf>=7.36; extra == 'proto'
43
41
  Provides-Extra: rand
44
42
  Requires-Dist: numpy>=2.5; extra == 'rand'
45
43
  Provides-Extra: rex
46
- Requires-Dist: irregex>=2.3.0; extra == 'rex'
44
+ Requires-Dist: irregex>=2.6.0; extra == 'rex'
47
45
  Provides-Extra: serde
48
46
  Requires-Dist: msgspec>=0.21; extra == 'serde'
49
47
  Requires-Dist: orjson>=3.12; extra == 'serde'
@@ -85,15 +83,15 @@ python3 -m pip install 'unstd[serde,ids,crypto]'
85
83
  | Group | Extra | Backend / fallback |
86
84
  |---|---|---|
87
85
  | [`serde`](src/unstd/serde/README.md) | `unstd[serde]` | orjson / msgspec / pybase64 → stdlib `json`/`base64` |
88
- | [`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 |
89
87
  | [`crypto`](src/unstd/crypto/README.md) | `unstd[crypto]` | blake3 (required; hashlib ships BLAKE2, a different algorithm) |
90
- | [`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]` |
91
89
  | [`model`](src/unstd/model/README.md) | `unstd[model]` | pydantic (required) |
92
90
  | [`rex`](src/unstd/rex/README.md) | `unstd[rex]` | irregex → stdlib `re` |
93
91
  | [`containers`](src/unstd/containers/README.md) | `unstd[containers]` | sortedcontainers + immutables (required) |
94
92
  | [`rand`](src/unstd/rand/README.md) | `unstd[rand]` | numpy PCG64 → stdlib `random`; `crypto` is always `secrets` |
95
93
  | [`pack`](src/unstd/pack/README.md) | `unstd[pack]` | numpy → stdlib `struct` |
96
- | [`clone`](src/unstd/clone/README.md) | `unstd[clone]` | msgspec → `copy.deepcopy` |
94
+ | [`clone`](src/unstd/clone/README.md) | — | stdlib; exact-type walk, `copy.deepcopy` per foreign node |
97
95
  | [`text`](src/unstd/text/README.md) | `unstd[text]` | rapidfuzz → stdlib `difflib` |
98
96
  | [`audio`](src/unstd/audio/README.md) | `unstd[audio]` | soundfile + numpy → stdlib `wave` |
99
97
  | [`toml`](src/unstd/toml/README.md) | `unstd[toml]` | read is `tomllib`; write requires tomlkit |
@@ -101,7 +99,7 @@ python3 -m pip install 'unstd[serde,ids,crypto]'
101
99
  | [`fs`](src/unstd/fs/README.md) | — | stdlib `os` |
102
100
  | [`iters`](src/unstd/iters/README.md) | — | stdlib `itertools` |
103
101
 
104
- `proc`, `fs`, and `iters` ship in the base install.
102
+ `clone`, `proc`, `fs`, and `iters` ship in the base install.
105
103
 
106
104
  ## Prior art
107
105
 
@@ -27,15 +27,15 @@ 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) |
36
36
  | [`rand`](src/unstd/rand/README.md) | `unstd[rand]` | numpy PCG64 → stdlib `random`; `crypto` is always `secrets` |
37
37
  | [`pack`](src/unstd/pack/README.md) | `unstd[pack]` | numpy → stdlib `struct` |
38
- | [`clone`](src/unstd/clone/README.md) | `unstd[clone]` | msgspec → `copy.deepcopy` |
38
+ | [`clone`](src/unstd/clone/README.md) | — | stdlib; exact-type walk, `copy.deepcopy` per foreign node |
39
39
  | [`text`](src/unstd/text/README.md) | `unstd[text]` | rapidfuzz → stdlib `difflib` |
40
40
  | [`audio`](src/unstd/audio/README.md) | `unstd[audio]` | soundfile + numpy → stdlib `wave` |
41
41
  | [`toml`](src/unstd/toml/README.md) | `unstd[toml]` | read is `tomllib`; write requires tomlkit |
@@ -43,7 +43,7 @@ python3 -m pip install 'unstd[serde,ids,crypto]'
43
43
  | [`fs`](src/unstd/fs/README.md) | — | stdlib `os` |
44
44
  | [`iters`](src/unstd/iters/README.md) | — | stdlib `itertools` |
45
45
 
46
- `proc`, `fs`, and `iters` ship in the base install.
46
+ `clone`, `proc`, `fs`, and `iters` ship in the base install.
47
47
 
48
48
  ## Prior art
49
49
 
@@ -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
@@ -101,6 +101,15 @@ The harness earned its keep on the first run:
101
101
  the algorithm, and it is not something a caller can act on.
102
102
  - **`clone.deep` on a small dict is 1.19×, not the 1.8× the README claimed.**
103
103
  The floor case is real and the README now says what the harness measures.
104
+ - **The 1.0.5 `clone.deep` floors were set against a clone that dropped types.**
105
+ Its msgspec round-trip turned a nested `StrEnum` into a `str` and a
106
+ `defaultdict` into a `dict`, and its equality guard could not tell. 1.1 replaced
107
+ it with a type-exact walker (4.4× on the large trees instead of 5.8×, 4.1× on
108
+ the small dict instead of 1.2×), and the two large-tree floors were re-pinned
109
+ by hand at the usual 70% of the new measurement. That is the one legitimate way
110
+ a floor comes down: the old number was bought with a bug.
111
+ - **`update --group X` used to erase every other group's floors.** It rewrote the
112
+ baseline from the narrowed run alone. It now merges, and a test pins that.
104
113
 
105
114
  ## Adding a case
106
115
 
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "_": "Speed floors, ratio of stdlib cost to unstd cost. Raised by `python3 -m bench update`, never lowered by it. See bench/README.md.",
3
- "recorded": "2026-09-01",
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"
@@ -28,19 +28,29 @@
28
28
  "measured": 1767.1,
29
29
  "group": "pack"
30
30
  },
31
+ "clone.asdict · dataclass tree": {
32
+ "floor": 7.55,
33
+ "measured": 10.78,
34
+ "group": "clone"
35
+ },
36
+ "clone.deep · 40 pydantic models": {
37
+ "floor": 1.58,
38
+ "measured": 2.25,
39
+ "group": "clone"
40
+ },
31
41
  "clone.deep · 500 records": {
32
- "floor": 4.09,
33
- "measured": 5.85,
42
+ "floor": 3.16,
43
+ "measured": 4.51,
34
44
  "group": "clone"
35
45
  },
36
46
  "clone.deep · nested (200 items)": {
37
- "floor": 4.13,
38
- "measured": 5.67,
47
+ "floor": 3.02,
48
+ "measured": 4.22,
39
49
  "group": "clone"
40
50
  },
41
51
  "clone.deep · small dict": {
42
- "floor": 0.85,
43
- "measured": 1.21,
52
+ "floor": 2.93,
53
+ "measured": 4.16,
44
54
  "group": "clone"
45
55
  },
46
56
  "draw.floats · 100k": {
@@ -68,7 +78,7 @@
68
78
  "measured": 41.87,
69
79
  "group": "text"
70
80
  },
71
- "hash.sum_ · 1 MiB": {
81
+ "digest.raw · 1 MiB": {
72
82
  "floor": 1.21,
73
83
  "measured": 1.72,
74
84
  "group": "ids"