diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..6324d40 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.14 diff --git a/Makefile b/Makefile index c38d2a6..f496157 100644 --- a/Makefile +++ b/Makefile @@ -23,6 +23,7 @@ EXAMPLE ?= all # Automatically find all benchmark names BENCHMARKS := $(patsubst %.zig,%,$(notdir $(wildcard benches/*.zig))) BENCHMARK ?= all +BENCH_ARGS ?= SHELL := /usr/bin/env bash .SHELLFLAGS := -eu -o pipefail -c @@ -68,11 +69,11 @@ bench: ## Run a benchmark (like 'make bench BENCHMARK=b1_btree_map' or 'make run for ex in $(BENCHMARKS); do \ echo ""; \ echo "--> Running '$$ex'"; \ - $(ZIG) build bench-$$ex $(BUILD_OPTS); \ + $(ZIG) build bench-$$ex $(BUILD_OPTS) -- $(BENCH_ARGS); \ done; \ else \ echo "--> Running benchmark: $(BENCHMARK)"; \ - $(ZIG) build bench-$(BENCHMARK) $(BUILD_OPTS); \ + $(ZIG) build bench-$(BENCHMARK) $(BUILD_OPTS) -- $(BENCH_ARGS); \ fi test: ## Run tests @@ -89,7 +90,7 @@ clean: ## Remove docs, build artifacts, and cache directories lint: ## Check code style and formatting of Zig files @echo "Running code style checks..." - @$(ZIG) fmt --check $(SRC_DIR) $(EXAMPLES_DIR) + @$(ZIG) fmt --check $(SRC_DIR) $(EXAMPLES_DIR) $(BENCHMARKS_DIR) build.zig format: ## Format Zig files @echo "Formatting Zig files..." diff --git a/README.md b/README.md index 5c915d8..a76f5ce 100644 --- a/README.md +++ b/README.md @@ -10,8 +10,8 @@ [![Benchmarks](https://img.shields.io/github/actions/workflow/status/CogitatorTech/ordered/benches.yml?label=benchmarks&style=flat&labelColor=282c34&logo=github)](https://github.com/CogitatorTech/ordered/actions/workflows/benches.yml) [![Zig](https://img.shields.io/badge/zig-0.17.0-F7A41D?style=flat&labelColor=282c34&logo=zig)](https://ziglang.org/download/) [![Release](https://img.shields.io/github/release/CogitatorTech/ordered.svg?label=release&style=flat&labelColor=282c34&logo=github)](https://github.com/CogitatorTech/ordered/releases/latest) -[![Docs](https://img.shields.io/badge/docs-read-blue?style=flat&labelColor=282c34&logo=read-the-docs)](https://CogitatorTech.github.io/ordered/) -[![Examples](https://img.shields.io/badge/examples-view-green?style=flat&labelColor=282c34&logo=zig)](https://github.com/CogitatorTech/ordered/tree/main/examples) +[![Docs](https://img.shields.io/badge/docs-read-507ec6?style=flat&labelColor=282c34&logo=read-the-docs)](https://CogitatorTech.github.io/ordered/) +[![Examples](https://img.shields.io/badge/examples-view-507ec6?style=flat&labelColor=282c34&logo=zig)](https://github.com/CogitatorTech/ordered/tree/main/examples) [![License](https://img.shields.io/badge/license-MIT-007ec6?label=license&style=flat&labelColor=282c34&logo=open-source-initiative)](https://github.com/CogitatorTech/ordered/blob/main/LICENSE) A sorted collection library for Zig diff --git a/benches/README.md b/benches/README.md index 616cf5f..9e423b4 100644 --- a/benches/README.md +++ b/benches/README.md @@ -10,6 +10,8 @@ | 4 | [b4_skip_list_map.zig](b4_skip_list_map.zig) | Benchmarks for Skip list map implementation | | 5 | [b5_trie_map.zig](b5_trie_map.zig) | Benchmarks for Trie map implementation | | 6 | [b6_cartesian_tree_map.zig](b6_cartesian_tree_map.zig) | Benchmarks for Cartesian tree map implementation | +| 7 | [b7_std_comparison.zig](b7_std_comparison.zig) | Timing comparisons with Zig standard-library containers | +| 8 | [b8_memory_comparison.zig](b8_memory_comparison.zig) | Memory comparisons with Zig standard-library containers | #### Running Benchmarks @@ -26,14 +28,128 @@ zig build bench-b1_btree_map ``` > [!NOTE] -> Each benchmark measures three core operations across multiple data sizes: +> The individual container benchmarks measure core operations across multiple data sizes: > 1. **Insert and Put**: measures the time to insert elements sequentially into an empty data structure > 2. **Lookup**: measures the time to search for all elements in a pre-populated structure > 3. **Delete**: measures the time to remove all elements from a pre-populated structure > > **Test Sizes**: benchmarks run with 1,000, 10,000, 100,000, and 1,000,000 elements to show performance scaling. > -> **Timing Method**: uses `std.time.Timer` for high-precision nanosecond-level timing. Each operation is timed in bulk, +> **Timing Method**: uses the local timer backed by `std.Io.Timestamp` on the monotonic `.real` clock. Each operation is timed in bulk, > then divided by the number of operations to get per-operation timing. > > **Compilation**: benchmarks are compiled with `ReleaseFast` optimization (see [build.zig](../build.zig)). + +#### Standard-Library Comparisons + +The timing and memory runners compare `BTreeMap` (branching factor 16), `CartesianTreeMap`, and `SkipListMap` +(maximum level 16) with Zig's `std.Treap` and hash maps. Integer keys use `std.AutoHashMap`; string keys use +`std.StringHashMap`. Hash maps have unordered iteration and serve as baselines for workloads that do not require +ordering. The intrusive `std.Treap` uses a local adapter that allocates a node and stores a value for each key. +Allocation and freeing are included in insertion and removal timings. + +```sh +# The complete default workloads, including batches of tiny maps. +make bench BENCHMARK=b7_std_comparison +make bench BENCHMARK=b8_memory_comparison + +# A short validation run of every key and value combination. +make bench BENCHMARK=b7_std_comparison BENCH_ARGS=--quick +make bench BENCHMARK=b8_memory_comparison BENCH_ARGS=--quick + +# Separate runs with 1M and 10M integer entries. +make bench BENCHMARK=b7_std_comparison BENCH_ARGS=--large +make bench BENCHMARK=b8_memory_comparison BENCH_ARGS=--large + +# Every large timing case with one sample after one warmup. +make bench BENCHMARK=b7_std_comparison BENCH_ARGS='--large --quick' + +# Equivalent direct Zig invocation. +zig build bench-b7_std_comparison -- --large +``` + +##### Workloads + +| Mode | Entries per Map | Keys | Values | Tiny Maps | +|------|-----------------|------|--------|-----------| +| Default | 1,000, 10,000, and 100,000 | `u64`, 16-byte strings, and 100-byte strings | `u64` and 32-byte records | 10,000 maps each of 4 and 32 entries | +| `--quick` | 1,000 | Same as default | Same as default | 128 maps each of 4 and 32 entries | +| `--large` | 1,000,000 and 10,000,000 | `u64` | `u64` | None | +| `--large --quick` | Same as large | `u64` | `u64` | None | + +All containers use `std.heap.smp_allocator` and ReleaseFast optimization for both the runner and imported library. +The large mode runs separately so routine benchmarks do not construct 10M-entry maps. String and larger-value +cases remain in the default workload. Tiny maps use 16-byte string keys and 32-byte records. + +Present integer keys are even numbers, and absent keys are odd numbers interleaved among them; the final absent +key is just above the maximum present key. String keys encode those same numbers as fixed-width hexadecimal text. +The 100-byte case pads that text with a shared prefix, exercising comparisons of long common prefixes. These are +synthetic keys, rather than samples from real applications. + +Each single-map workload runs with ascending and shuffled insertion orders. Lookup and removal each use a separate +permutation generated with a different fixed seed, regardless of insertion order. Every container receives the +same workload. Container priorities and skip-list levels use normal initialization and may vary between runs. +Key buffers and operation permutations are generated before measurement and kept alive until all containers finish. +Strings are borrowed from shared buffers; string payload allocation and copying are excluded. +Successful lookup and removal use separately stored, byte-identical string keys, so pointer equality cannot bypass +content comparison. + +##### Timing Measurements + +The timing runner measures insertion, successful lookup, unsuccessful lookup, full iteration, and removal. Lookups +use `getPtr` consistently, and successful lookups and iteration consume every word of the stored value. Result sums, +hit counts, removal counts, and container sizes are checked outside timed regions. Each phase runs once per map; +lookup, iteration, and removal follow insertion on the same populated container. Iterator creation and cleanup are +included in iteration time. Container initialization and final cleanup are excluded for single-map workloads. +Insertion and iteration visit every entry. Lookup and removal each visit at most 100,000 keys from their independent +permutations. Their timings are divided by the number of sampled keys, and the remaining entries are freed outside +the removal timer. This keeps large-map queries bounded while preserving the full working set. Default and quick +workloads visit every key because their maps fit within that limit. + +Tiny-map workloads measure construction and destruction in nanoseconds per map, and iteration in nanoseconds per +item, while all maps coexist. Construction includes the map array allocation, initialization, and insertion; +destruction includes freeing all containers and the array. Maps share the same key buffers and insertion order. + +Default and large timing runs report the median of five samples after one warmup. Adding `--quick`, including with +`--large`, uses one sample after one warmup for validation. CSV output goes to standard error, preceded by a metadata line. Tiny +results form a second section with their own header and timing columns. + +##### Memory Measurements + +The memory runner performs no timing. A counting allocator wraps `smp_allocator` during construction and records +live requested bytes after construction, peak requested bytes during construction, and live bytes per entry. +It includes each container's inline storage by allocating an array of containers, including for a single map. +Each workload is built once, and every measured allocation must be released after destruction. + +Shared string buffers, operation permutations, allocator metadata, size-class rounding, reserved pages, and process +RSS are excluded. The string payload cost is shared equally by all containers, so string length alone does not +change reported container memory. Peak bytes describe allocations visible through Zig's allocator interface, not +internal temporary allocations made by the underlying allocator. Memory instrumentation is never used by the timing +runner. Repeated runs may differ for randomized containers. + +##### Sample Results + +The following results were measured on Linux x86_64 with an AMD Ryzen 9 9950X and Zig 0.17.0 on October 25, 2026, +using the native target. Each map held 100,000 shuffled `u64` keys and `u64` values. Timings are nanoseconds per +operation, except iteration, which is nanoseconds per item. Memory is live requested bytes per item from a separate +construction run, including inline container storage and excluding shared workload buffers. + +| Container | Insert | Lookup Hit | Lookup Miss | Iteration | Remove | Bytes per Item | +|-----------|-------:|-----------:|------------:|----------:|-------:|---------------:| +| `BTreeMap` | 86.30 | 123.21 | 134.28 | 2.52 | 130.85 | 36.83 | +| `CartesianTreeMap` | 222.54 | 141.05 | 154.62 | 9.08 | 159.50 | 40.00 | +| `SkipListMap` | 195.68 | 202.16 | 197.31 | 18.43 | 194.13 | 47.99 | +| `std.Treap` | 137.92 | 102.40 | 102.92 | 15.26 | 129.29 | 48.00 | +| `std.AutoHashMap` (unordered) | 29.11 | 10.61 | 19.91 | 2.43 | 11.60 | 22.28 | + +##### Validation and Limits + +`make test` includes benchmark tests for adapters, both value sizes, both string lengths, independent permutations, +interleaved misses, duplicate updates, missing removals, empty iteration, allocator accounting, and cleanup after +allocation failures. A workload above the query limit checks sampled lookups and partial removal. `make lint` +includes benchmark sources and `build.zig`. + +Measurements are single-threaded, with no CPU pinning or frequency control. This suite does not measure concurrent +access, interleaved mutations, prefix queries, or range scans. It adds no external dependencies and includes no Rust +or C libraries. Results from the earlier `u32` comparison are not comparable with these workloads and have been +removed from this document. diff --git a/benches/b7_std_comparison.zig b/benches/b7_std_comparison.zig new file mode 100644 index 0000000..e84dbf1 --- /dev/null +++ b/benches/b7_std_comparison.zig @@ -0,0 +1,6 @@ +const std = @import("std"); +const comparison = @import("util/comparison.zig"); + +pub fn main(init: std.process.Init) !void { + try comparison.run(init, false); +} diff --git a/benches/b8_memory_comparison.zig b/benches/b8_memory_comparison.zig new file mode 100644 index 0000000..4e8b3bd --- /dev/null +++ b/benches/b8_memory_comparison.zig @@ -0,0 +1,6 @@ +const std = @import("std"); +const comparison = @import("util/comparison.zig"); + +pub fn main(init: std.process.Init) !void { + try comparison.run(init, true); +} diff --git a/benches/util/comparison.zig b/benches/util/comparison.zig new file mode 100644 index 0000000..3435cba --- /dev/null +++ b/benches/util/comparison.zig @@ -0,0 +1,452 @@ +const std = @import("std"); +const ordered = @import("ordered"); +const Timer = @import("timer.zig").Timer; +const CountingAllocator = @import("counting_allocator.zig").CountingAllocator; + +const PROBE_LIMIT = 100_000; +const NAMES = .{ "BTreeMap", "CartesianTreeMap", "SkipListMap", "std.Treap", "std.HashMap (unordered)" }; + +fn check(ok: bool) !void { + if (!ok) return error.BenchmarkMismatch; +} + +fn shuffle(items: []u32, seed: u64) void { + var prng = std.Random.DefaultPrng.init(seed); + prng.random().shuffle(u32, items); +} + +fn Suite(comptime K: type, comptime V: type) type { + return struct { + const BTree = ordered.BTreeMap(K, V, compare, 16); + const CartesianTree = ordered.CartesianTreeMap(K, V, compare); + const SkipList = ordered.SkipListMap(K, V, compare, 16); + const HashMap = if (K == u64) std.AutoHashMap(K, V) else std.StringHashMap(V); + const MAPS = .{ BTree, CartesianTree, SkipList, TreapMap, HashMap }; + + fn compare(a: K, b: K) std.math.Order { + return if (K == u64) std.math.order(a, b) else std.mem.order(u8, a, b); + } + + fn value(id: u64) V { + return if (V == u64) id else .{ id, id * 3, id * 5, id * 7 }; + } + + fn fingerprint(v: V) u64 { + return if (V == u64) v else v[0] + v[1] + v[2] + v[3]; + } + + fn expectedSum(size: usize) u64 { + return @as(u64, size) * (size + 1) / 2 * (if (V == u64) @as(u64, 1) else 16); + } + + // std.Treap is intrusive; include allocation and value storage in its cost. + const TreapMap = struct { + const Tree = std.Treap(K, compare); + const Node = struct { tree_node: Tree.Node, value: V }; + allocator: std.mem.Allocator, + tree: Tree = .{}, + len: usize = 0, + + fn init(allocator: std.mem.Allocator) TreapMap { + return .{ .allocator = allocator }; + } + + fn deinit(self: *TreapMap) void { + while (self.tree.getMin()) |node| _ = self.remove(node.key); + } + + fn count(self: *const TreapMap) usize { + return self.len; + } + + fn put(self: *TreapMap, key: K, v: V) !void { + var entry = self.tree.getEntryFor(key); + if (entry.node) |node| { + const parent: *Node = @fieldParentPtr("tree_node", node); + parent.value = v; + } else { + const node = try self.allocator.create(Node); + node.value = v; + entry.set(&node.tree_node); + self.len += 1; + } + } + + fn getPtr(self: *TreapMap, key: K) ?*V { + const node = self.tree.getEntryFor(key).node orelse return null; + const parent: *Node = @fieldParentPtr("tree_node", node); + return &parent.value; + } + + fn remove(self: *TreapMap, key: K) ?V { + var entry = self.tree.getEntryFor(key); + const node = entry.node orelse return null; + const parent: *Node = @fieldParentPtr("tree_node", node); + const v = parent.value; + entry.set(null); + self.allocator.destroy(parent); + self.len -= 1; + return v; + } + }; + + const Dataset = struct { + present: []K, + absent: []K, + queries: []K, + bytes: []u8, + insertion: []u32, + lookup: []u32, + removal: []u32, + + fn init(allocator: std.mem.Allocator, size: usize, key_len: usize) !Dataset { + const present = try allocator.alloc(K, size); + errdefer allocator.free(present); + const absent = try allocator.alloc(K, size); + errdefer allocator.free(absent); + const queries = try allocator.alloc(K, size); + errdefer allocator.free(queries); + const bytes = try allocator.alloc(u8, if (K == u64) 0 else size * 3 * key_len); + errdefer allocator.free(bytes); + for (0..size) |i| { + if (K == u64) { + present[i] = 2 * i; + absent[i] = 2 * i + 1; + queries[i] = present[i]; + } else { + for (0..3) |which| { + const key = bytes[(3 * i + which) * key_len ..][0..key_len]; + @memset(key, 'a'); + _ = try std.fmt.bufPrint(key[key_len - 16 ..], "{x:0>16}", .{2 * i + which % 2}); + switch (which) { + 0 => present[i] = key, + 1 => absent[i] = key, + 2 => queries[i] = key, + else => unreachable, + } + } + } + } + const insertion = try allocator.alloc(u32, size); + errdefer allocator.free(insertion); + const lookup = try allocator.alloc(u32, size); + errdefer allocator.free(lookup); + const removal = try allocator.alloc(u32, size); + errdefer allocator.free(removal); + for (0..size) |i| { + insertion[i] = @intCast(i); + lookup[i] = @intCast(i); + removal[i] = @intCast(i); + } + shuffle(lookup, 0x10_0c); + shuffle(removal, 0xde_1e); + return .{ .present = present, .absent = absent, .queries = queries, .bytes = bytes, .insertion = insertion, .lookup = lookup, .removal = removal }; + } + + fn deinit(self: Dataset, allocator: std.mem.Allocator) void { + allocator.free(self.removal); + allocator.free(self.lookup); + allocator.free(self.insertion); + allocator.free(self.bytes); + allocator.free(self.queries); + allocator.free(self.absent); + allocator.free(self.present); + } + }; + + fn initMap(comptime Map: type, allocator: std.mem.Allocator) !Map { + return if (Map == SkipList) try Map.init(allocator) else Map.init(allocator); + } + + fn remove(comptime Map: type, map: *Map, key: K) bool { + return if (Map == HashMap) map.remove(key) else map.remove(key) != null; + } + + fn iterationSum(comptime Map: type, map: *Map, allocator: std.mem.Allocator) !u64 { + var sum: u64 = 0; + if (Map == TreapMap) { + var it = map.tree.inorderIterator(); + while (it.next()) |node| { + const parent: *TreapMap.Node = @fieldParentPtr("tree_node", node); + sum += fingerprint(parent.value); + } + } else if (Map == HashMap) { + var it = map.iterator(); + while (it.next()) |entry| sum += fingerprint(entry.value_ptr.*); + } else if (Map == SkipList) { + var it = map.iterator(); + while (it.next()) |entry| sum += fingerprint(entry.value); + } else { + var it = if (Map == CartesianTree) try map.iterator(allocator) else try map.iterator(); + defer it.deinit(); + while (try it.next()) |entry| sum += fingerprint(entry.value); + } + return sum; + } + + fn measure(comptime Map: type, allocator: std.mem.Allocator, data: Dataset) ![5]u64 { + var map = try initMap(Map, allocator); + defer map.deinit(); + var times: [5]u64 = undefined; + var timer = try Timer.start(); + for (data.insertion) |i| try map.put(data.present[i], value(@as(u64, i) + 1)); + times[0] = timer.read(); + try check(map.count() == data.present.len); + const probes = data.lookup[0..@min(data.lookup.len, PROBE_LIMIT)]; + var expected_hits: u64 = 0; + for (probes) |i| expected_hits += fingerprint(value(@as(u64, i) + 1)); + var sum: u64 = 0; + var hits: usize = 0; + _ = timer.lap(); + for (probes) |i| { + if (map.getPtr(data.queries[i])) |v| { + sum += fingerprint(v.*); + hits += 1; + } + } + times[1] = timer.read(); + try check(hits == probes.len and sum == expected_hits); + hits = 0; + _ = timer.lap(); + for (probes) |i| { + if (map.getPtr(data.absent[i]) != null) hits += 1; + } + times[2] = timer.read(); + try check(hits == 0); + _ = timer.lap(); + sum = try iterationSum(Map, &map, allocator); + times[3] = timer.read(); + try check(sum == expectedSum(data.present.len)); + var removed: usize = 0; + _ = timer.lap(); + for (data.removal[0..probes.len]) |i| { + if (remove(Map, &map, data.queries[i])) removed += 1; + } + times[4] = timer.read(); + try check(removed == probes.len and map.count() == data.present.len - probes.len); + return times; + } + + fn buildMany(comptime Map: type, allocator: std.mem.Allocator, data: Dataset, count: usize) ![]Map { + const maps = try allocator.alloc(Map, count); + errdefer allocator.free(maps); + var initialized: usize = 0; + errdefer for (maps[0..initialized]) |*map| map.deinit(); + for (maps) |*map| { + map.* = try initMap(Map, allocator); + initialized += 1; + for (data.insertion) |i| try map.put(data.present[i], value(@as(u64, i) + 1)); + } + return maps; + } + + fn destroyMany(comptime Map: type, allocator: std.mem.Allocator, maps: []Map) void { + for (maps) |*map| map.deinit(); + allocator.free(maps); + } + + fn measureTiny(comptime Map: type, allocator: std.mem.Allocator, data: Dataset, count: usize) ![3]u64 { + var timer = try Timer.start(); + const maps = try buildMany(Map, allocator, data, count); + const build_ns = timer.read(); + errdefer destroyMany(Map, allocator, maps); + var sum: u64 = 0; + _ = timer.lap(); + for (maps) |*map| sum += try iterationSum(Map, map, allocator); + const iteration_ns = timer.read(); + try check(sum == expectedSum(data.present.len) * count); + _ = timer.lap(); + destroyMany(Map, allocator, maps); + const destroy_ns = timer.read(); + return .{ build_ns, iteration_ns, destroy_ns }; + } + + fn selfCheck(allocator: std.mem.Allocator, key_len: usize) !void { + const data = try Dataset.init(allocator, 17, key_len); + defer data.deinit(allocator); + inline for (MAPS) |Map| { + var map = try initMap(Map, allocator); + defer map.deinit(); + try check(map.count() == 0 and map.getPtr(data.present[0]) == null); + try check(try iterationSum(Map, &map, allocator) == 0); + try check(!remove(Map, &map, data.present[0])); + try map.put(data.present[0], value(1)); + try map.put(data.queries[0], value(2)); + try check(map.count() == 1 and fingerprint(map.getPtr(data.queries[0]).?.*) == fingerprint(value(2))); + try check(remove(Map, &map, data.queries[0]) and map.count() == 0); + // Leave a live node to exercise cleanup as well as removal. + try map.put(data.present[1], value(3)); + _ = try measure(Map, allocator, data); + _ = try measureTiny(Map, allocator, data, 3); + } + } + + fn report(allocator: std.mem.Allocator, size: usize, key_len: usize, repeats: usize, comptime memory: bool, comptime tiny: bool, tiny_count: usize) !void { + const data = try Dataset.init(allocator, size, key_len); + defer data.deinit(allocator); + for (0..if (tiny) @as(usize, 1) else 2) |input_index| { + if (input_index == 1 or tiny) shuffle(data.insertion, 0x1a_5e); + inline for (MAPS, NAMES) |Map, name| { + const input = if (input_index == 1 or tiny) "shuffled" else "sequential"; + const key_kind = if (K == u64) "u64" else if (key_len == 16) "string16" else "string100"; + std.debug.print("{s},{s},{d},{d},{s},{d}", .{ name, key_kind, @sizeOf(V), size, input, @max(tiny_count, 1) }); + if (memory) { + var counting: CountingAllocator = .{ .child = allocator }; + var live: usize = 0; + var peak: usize = 0; + { + const maps = try buildMany(Map, counting.allocator(), data, @max(tiny_count, 1)); + defer destroyMany(Map, counting.allocator(), maps); + live = counting.live_bytes; + peak = counting.peak_bytes; + for (maps) |map| try check(map.count() == size); + } + try check(counting.live_bytes == 0); + const per_entry = @as(f64, @floatFromInt(live)) / @as(f64, @floatFromInt(size * @max(tiny_count, 1))); + std.debug.print(",{d},{d},{d:.2}\n", .{ live, peak, per_entry }); + } else { + const operation_count = if (tiny) 3 else 5; + var samples: [operation_count][5]u64 = undefined; + for (0..repeats + 1) |repeat| { + const times = if (tiny) try measureTiny(Map, allocator, data, tiny_count) else try measure(Map, allocator, data); + if (repeat > 0) for (times, 0..) |time, operation| { + samples[operation][repeat - 1] = time; + }; + } + for (&samples, 0..) |*operation, i| { + std.mem.sort(u64, operation[0..repeats], {}, std.sort.asc(u64)); + const divisor = if (!tiny) + (if (i == 1 or i == 2 or i == 4) @min(size, PROBE_LIMIT) else size) + else if (i == 1) size * tiny_count else tiny_count; + const per_op = @as(f64, @floatFromInt(operation[repeats / 2])) / @as(f64, @floatFromInt(divisor)); + std.debug.print(",{d:.2}", .{per_op}); + } + std.debug.print("\n", .{}); + } + } + } + } + }; +} + +pub fn run(init: std.process.Init, comptime memory: bool) !void { + const args = try init.minimal.args.toSlice(init.arena.allocator()); + var quick = false; + var large = false; + for (args[1..]) |arg| { + if (std.mem.eql(u8, arg, "--quick")) { + quick = true; + } else if (std.mem.eql(u8, arg, "--large")) { + large = true; + } else { + std.debug.print("Usage: {s} [--quick] [--large]\n", .{args[0]}); + return error.InvalidArguments; + } + } + const allocator = std.heap.smp_allocator; + const repeats: usize = if (quick) 1 else 5; + const sizes: []const usize = if (large) &.{ 1_000_000, 10_000_000 } else if (quick) &.{1_000} else &.{ 1_000, 10_000, 100_000 }; + if (memory) { + std.debug.print("ReleaseFast; smp_allocator; requested live and peak bytes; shared key buffers excluded\n", .{}); + } else { + std.debug.print("ReleaseFast; smp_allocator; median timings; samples={d}; warmups=1; query_limit={d}\n", .{ repeats, PROBE_LIMIT }); + } + std.debug.print("container,keys,value_bytes,items,input,maps,{s}\n", .{if (memory) "live_bytes,peak_bytes,bytes/item" else "insert_ns/op,hit_ns/op,miss_ns/op,iterate_ns/item,remove_ns/op"}); + for (sizes) |size| { + try Suite(u64, u64).report(allocator, size, 0, repeats, memory, false, 0); + if (!large) { + try Suite(u64, [4]u64).report(allocator, size, 0, repeats, memory, false, 0); + inline for (.{ u64, [4]u64 }) |V| { + for ([_]usize{ 16, 100 }) |key_len| try Suite([]const u8, V).report(allocator, size, key_len, repeats, memory, false, 0); + } + } + } + if (!large) { + std.debug.print("container,keys,value_bytes,items,input,maps,{s}\n", .{if (memory) "live_bytes,peak_bytes,bytes/item" else "build_ns/map,iterate_ns/item,destroy_ns/map"}); + for ([_]usize{ 4, 32 }) |size| try Suite([]const u8, [4]u64).report(allocator, size, 16, repeats, memory, true, if (quick) 128 else 10_000); + } +} + +test "comparison workloads cover map adapters, value sizes, and cleanup" { + inline for (.{ u64, [4]u64 }) |V| { + try Suite(u64, V).selfCheck(std.testing.allocator, 0); + try Suite([]const u8, V).selfCheck(std.testing.allocator, 16); + try Suite([]const u8, V).selfCheck(std.testing.allocator, 100); + } +} + +test "missing keys are interleaved and operation orders are independent permutations" { + const S = Suite(u64, u64); + const data = try S.Dataset.init(std.testing.allocator, 100, 0); + defer data.deinit(std.testing.allocator); + var seen: [100]bool = @splat(false); + for (data.present, data.absent, 0..) |present, absent, i| { + try std.testing.expectEqual(2 * i, present); + try std.testing.expectEqual(present + 1, absent); + } + for (data.lookup) |i| { + try std.testing.expect(!seen[i]); + seen[i] = true; + } + @memset(&seen, false); + for (data.removal) |i| { + try std.testing.expect(!seen[i]); + seen[i] = true; + } + try std.testing.expect(!std.mem.eql(u32, data.insertion, data.lookup)); + shuffle(data.insertion, 0x1a_5e); + try std.testing.expect(!std.mem.eql(u32, data.insertion, data.lookup)); + try std.testing.expect(!std.mem.eql(u32, data.insertion, data.removal)); + try std.testing.expect(!std.mem.eql(u32, data.lookup, data.removal)); +} + +fn allocationCheck(allocator: std.mem.Allocator) !void { + const S = Suite([]const u8, [4]u64); + const data = try S.Dataset.init(allocator, 4, 16); + defer data.deinit(allocator); + inline for (S.MAPS) |Map| { + const maps = try S.buildMany(Map, allocator, data, 3); + S.destroyMany(Map, allocator, maps); + } +} + +test "dataset and partially built maps release allocations on failure" { + try std.testing.checkAllAllocationFailures(std.testing.allocator, allocationCheck, .{}); +} + +test "large workloads check sampled lookups and partial removal" { + const S = Suite(u64, u64); + const data = try S.Dataset.init(std.testing.allocator, PROBE_LIMIT + 3, 0); + defer data.deinit(std.testing.allocator); + _ = try S.measure(S.BTree, std.testing.allocator, data); + _ = try S.measure(S.HashMap, std.testing.allocator, data); +} + +test "memory workload releases all measured allocations" { + const S = Suite([]const u8, [4]u64); + const data = try S.Dataset.init(std.testing.allocator, 32, 16); + defer data.deinit(std.testing.allocator); + inline for (S.MAPS) |Map| { + var counting: CountingAllocator = .{ .child = std.testing.allocator }; + const maps = try S.buildMany(Map, counting.allocator(), data, 4); + try std.testing.expect(counting.live_bytes >= @sizeOf(Map) * 4); + try std.testing.expect(counting.peak_bytes >= counting.live_bytes); + S.destroyMany(Map, counting.allocator(), maps); + try std.testing.expectEqual(0, counting.live_bytes); + } +} + +test "string probes have equal contents at different addresses and misses sort between present keys" { + const S = Suite([]const u8, u64); + for ([_]usize{ 16, 100 }) |key_len| { + const data = try S.Dataset.init(std.testing.allocator, 17, key_len); + defer data.deinit(std.testing.allocator); + for (data.present, data.queries, data.absent, 0..) |present, query, absent, i| { + try std.testing.expectEqual(key_len, present.len); + try std.testing.expect(present.ptr != query.ptr); + try std.testing.expectEqualStrings(present, query); + try std.testing.expect(std.mem.order(u8, present, absent) == .lt); + if (i + 1 < data.present.len) try std.testing.expect(std.mem.order(u8, absent, data.present[i + 1]) == .lt); + } + } +} diff --git a/benches/util/counting_allocator.zig b/benches/util/counting_allocator.zig new file mode 100644 index 0000000..47238bb --- /dev/null +++ b/benches/util/counting_allocator.zig @@ -0,0 +1,87 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const Alignment = std.mem.Alignment; + +pub const CountingAllocator = struct { + child: Allocator, + live_bytes: usize = 0, + peak_bytes: usize = 0, + + pub fn allocator(self: *CountingAllocator) Allocator { + return .{ .ptr = self, .vtable = &.{ .alloc = alloc, .resize = resize, .remap = remap, .free = free } }; + } + + fn account(self: *CountingAllocator, old_len: usize, new_len: usize) void { + self.live_bytes = self.live_bytes - old_len + new_len; + self.peak_bytes = @max(self.peak_bytes, self.live_bytes); + } + + fn alloc(ctx: *anyopaque, len: usize, alignment: Alignment, ret_addr: usize) ?[*]u8 { + const self: *CountingAllocator = @ptrCast(@alignCast(ctx)); + const ptr = self.child.rawAlloc(len, alignment, ret_addr) orelse return null; + self.account(0, len); + return ptr; + } + + fn resize(ctx: *anyopaque, memory: []u8, alignment: Alignment, new_len: usize, ret_addr: usize) bool { + const self: *CountingAllocator = @ptrCast(@alignCast(ctx)); + if (!self.child.rawResize(memory, alignment, new_len, ret_addr)) return false; + self.account(memory.len, new_len); + return true; + } + + fn remap(ctx: *anyopaque, memory: []u8, alignment: Alignment, new_len: usize, ret_addr: usize) ?[*]u8 { + const self: *CountingAllocator = @ptrCast(@alignCast(ctx)); + const ptr = self.child.rawRemap(memory, alignment, new_len, ret_addr) orelse return null; + self.account(memory.len, new_len); + return ptr; + } + + fn free(ctx: *anyopaque, memory: []u8, alignment: Alignment, ret_addr: usize) void { + const self: *CountingAllocator = @ptrCast(@alignCast(ctx)); + self.child.rawFree(memory, alignment, ret_addr); + self.account(memory.len, 0); + } +}; + +test "counting allocator tracks allocations, resizing, remapping, and freeing" { + var counting: CountingAllocator = .{ .child = std.testing.allocator }; + const allocator = counting.allocator(); + var bytes = try allocator.alloc(u8, 100); + try std.testing.expectEqual(100, counting.live_bytes); + if (allocator.resize(bytes, 50)) bytes = bytes[0..50]; + try std.testing.expectEqual(bytes.len, counting.live_bytes); + if (allocator.remap(bytes, 200)) |moved| bytes = moved; + try std.testing.expectEqual(bytes.len, counting.live_bytes); + bytes = try allocator.realloc(bytes, 400); + try std.testing.expectEqual(400, counting.live_bytes); + try std.testing.expect(counting.peak_bytes >= 400); + allocator.free(bytes); + try std.testing.expectEqual(0, counting.live_bytes); +} + +test "failed allocation does not change counts" { + var counting: CountingAllocator = .{ .child = std.testing.failing_allocator }; + try std.testing.expectError(error.OutOfMemory, counting.allocator().alloc(u8, 1)); + try std.testing.expectEqual(0, counting.live_bytes); + try std.testing.expectEqual(0, counting.peak_bytes); +} + +test "successful and failed size changes preserve accounting" { + var buffer: [512]u8 = undefined; + var fixed = std.heap.FixedBufferAllocator.init(&buffer); + var counting: CountingAllocator = .{ .child = fixed.allocator() }; + const allocator = counting.allocator(); + var bytes = try allocator.alloc(u8, 256); + try std.testing.expect(allocator.resize(bytes, 128)); + bytes = bytes[0..128]; + try std.testing.expectEqual(128, counting.live_bytes); + bytes = allocator.remap(bytes, 200) orelse return error.TestUnexpectedResult; + try std.testing.expectEqual(200, counting.live_bytes); + try std.testing.expect(!allocator.resize(bytes, 1024)); + try std.testing.expect(allocator.remap(bytes, 1024) == null); + try std.testing.expectEqual(200, counting.live_bytes); + try std.testing.expectEqual(256, counting.peak_bytes); + allocator.free(bytes); + try std.testing.expectEqual(0, counting.live_bytes); +} diff --git a/build.zig b/build.zig index 9196a2f..d6583b6 100644 --- a/build.zig +++ b/build.zig @@ -74,6 +74,20 @@ pub fn build(b: *std.Build) void { // --- Benchmarks --- const benches_path = "benches"; if (b.root.openDir(io, benches_path, .{ .iterate = true })) |benches_dir| { + const bench_test_module = b.createModule(.{ + .root_source_file = b.path("benches/util/comparison.zig"), + .target = target, + .optimize = optimize, + }); + bench_test_module.addImport("ordered", lib_module); + const bench_tests = b.addTest(.{ .root_module = bench_test_module }); + const run_bench_tests = b.addRunArtifact(bench_tests); + test_step.dependOn(&run_bench_tests.step); + const bench_lib_module = b.createModule(.{ + .root_source_file = lib_source, + .target = target, + .optimize = .ReleaseFast, + }); b.dependOnDirectoryContents(b.path(benches_path)); var dir = benches_dir; defer dir.close(io); @@ -89,13 +103,14 @@ pub fn build(b: *std.Build) void { .target = target, .optimize = .ReleaseFast, // Use ReleaseFast for benchmarks }); - bench_module.addImport("ordered", lib_module); + bench_module.addImport("ordered", bench_lib_module); const bench_exe = b.addExecutable(.{ .name = bench_name, .root_module = bench_module, }); b.installArtifact(bench_exe); const run_bench_cmd = b.addRunArtifact(bench_exe); + run_bench_cmd.addPassthruArgs(); const bench_step_name = b.fmt("bench-{s}", .{bench_name}); const bench_step_desc = b.fmt("Run the {s} benchmark", .{bench_name}); const bench_step = b.step(bench_step_name, bench_step_desc); diff --git a/pyproject.toml b/pyproject.toml index aa85766..519269c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ name = "ordered" version = "0.1.0" description = "The Python environment for the Ordered project" -requires-python = ">=3.10,<4.0" +requires-python = ">=3.14,<4.0" dependencies = [ "python-dotenv>=1.1.0", "pre-commit>=4.2.0" diff --git a/src/ordered/skip_list_map.zig b/src/ordered/skip_list_map.zig index b13dd1c..6fee965 100644 --- a/src/ordered/skip_list_map.zig +++ b/src/ordered/skip_list_map.zig @@ -50,6 +50,7 @@ pub fn SkipListMap( fn init(allocator: std.mem.Allocator, key: K, value: V, level: u8) !*Node { const node = try allocator.create(Node); + errdefer allocator.destroy(node); node.key = key; node.value = value; node.forward = try allocator.alloc(?*Node, level + 1); @@ -82,6 +83,7 @@ pub fn SkipListMap( /// Returns `error.OutOfMemory` if allocation fails. pub fn init(allocator: std.mem.Allocator) !Self { const header = try allocator.create(Node); + errdefer allocator.destroy(header); header.key = undefined; header.value = undefined; header.forward = try allocator.alloc(?*Node, MAX_LEVEL); @@ -565,3 +567,14 @@ test "SkipListMap: minimum max level" { try std.testing.expectEqual(@as(i32, 2), list.get(2).?.*); } + +fn allocationFailureCheck(allocator: std.mem.Allocator) !void { + var list = try SkipListMap(i32, i32, i32Compare, 16).init(allocator); + defer list.deinit(); + try list.put(1, 10); + try list.put(2, 20); +} + +test "SkipListMap: allocation failures release partial nodes" { + try std.testing.checkAllAllocationFailures(std.testing.allocator, allocationFailureCheck, .{}); +} diff --git a/uv.lock b/uv.lock index 585a860..33287fc 100644 --- a/uv.lock +++ b/uv.lock @@ -1,6 +1,6 @@ version = 1 -revision = 2 -requires-python = ">=3.10, <4.0" +revision = 3 +requires-python = ">=3.14, <4.0" [[package]] name = "cfgv" @@ -13,38 +13,38 @@ wheels = [ [[package]] name = "distlib" -version = "0.4.3" +version = "0.4.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c9/02/bd72be9134d25ed783ecbbc38a539ffaefbf90c78418c7fb7229600dbac7/distlib-0.4.3.tar.gz", hash = "sha256:f152097224a0ae24be5a0f6bae1b9359af82133bce63f98a95f86cae1aede9ed", size = 615141, upload-time = "2026-06-12T08:04:52.847Z" } +sdist = { url = "https://files.pythonhosted.org/packages/68/bc/d36966de7304047a1001a7559fbf266c464bb5d8cc85464536ef8c9bcc32/distlib-0.4.4.tar.gz", hash = "sha256:ca4175b4ac7f71223cc30ed7248c45d2b38c2439d8ee6e498ab67ed63cf3508d", size = 621304, upload-time = "2026-10-16T13:56:34.203Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/02/08/9c41fb51ab5b43eb21674aff13df270e8ba6c4b29c8624e328dc7a9482af/distlib-0.4.3-py2.py3-none-any.whl", hash = "sha256:4b0ce306c966eb73bc3a7b6abad017c556dadd92c44701562cd528ac7fde4d5b", size = 470628, upload-time = "2026-06-12T08:04:50.506Z" }, + { url = "https://files.pythonhosted.org/packages/c1/2b/238f02a4927fa2e27aa995371d240a468b52e42eae44a678434aff736b38/distlib-0.4.4-py2.py3-none-any.whl", hash = "sha256:cf09d55b1e85a89576b036aeb71550bfd68320f3f21761ca329eadadad918638", size = 472099, upload-time = "2026-10-16T13:56:32.053Z" }, ] [[package]] name = "filelock" -version = "3.29.4" +version = "4.2.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e6/dc/be6cbe99670cd6e4ad387123647cb08e0c32975e223f82551e914c5568a6/filelock-3.29.4.tar.gz", hash = "sha256:10cdb3656fc44541cdf30652a93fb10ec6b05325620eb316bd26893e4201538a", size = 63028, upload-time = "2026-06-13T16:12:00.744Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/28/3893324bcadf8f89b1616637468d766af3f625008c0735bf704fdfa86617/filelock-4.2.4.tar.gz", hash = "sha256:c6d8663535e1650ee430507b4ffbac7b464dd64b7647ea0112a9f406499d163d", size = 652444, upload-time = "2026-10-19T01:57:51.001Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/13/37/a065dc3bd6e49423a6532c642ca7378d3f467b1ef44c2800c937af7f9739/filelock-3.29.4-py3-none-any.whl", hash = "sha256:dac1648087d5115554850d113e7dd8c83ab2d38e3435dde2d4f163847e57b767", size = 42757, upload-time = "2026-06-13T16:11:59.582Z" }, + { url = "https://files.pythonhosted.org/packages/e0/1b/5e74289a1878e666bb4b65fe5805a04cafcac2aa5f519ef1ead67ef91c56/filelock-4.2.4-py3-none-any.whl", hash = "sha256:301241438c57936ef93f4243a94f2559022268bfcf9b375a9a72eb0f8db8358e", size = 162475, upload-time = "2026-10-19T01:57:49.419Z" }, ] [[package]] name = "identify" -version = "2.6.19" +version = "2.6.20" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/52/63/51723b5f116cc04b061cb6f5a561790abf249d25931d515cd375e063e0f4/identify-2.6.19.tar.gz", hash = "sha256:6be5020c38fcb07da56c53733538a3081ea5aa70d36a156f83044bfbf9173842", size = 99567, upload-time = "2026-04-17T18:39:50.265Z" } +sdist = { url = "https://files.pythonhosted.org/packages/53/35/d70c0006c7cee65999ea94a6273e60b2094f600a3d8b71b04318253fc643/identify-2.6.20.tar.gz", hash = "sha256:ad729860a923858d26917c2f4fb0a1d83d27a75b1e090c06440c573f048f3285", size = 99681, upload-time = "2026-09-26T20:29:07.187Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/94/84/d9273cd09688070a6523c4aee4663a8538721b2b755c4962aafae0011e72/identify-2.6.19-py2.py3-none-any.whl", hash = "sha256:20e6a87f786f768c092a721ad107fc9df0eb89347be9396cadf3f4abbd1fb78a", size = 99397, upload-time = "2026-04-17T18:39:49.221Z" }, + { url = "https://files.pythonhosted.org/packages/fa/70/fffc9613501877c0a10a1ef73a165e6ded3e53f6e7502227fef56d973933/identify-2.6.20-py2.py3-none-any.whl", hash = "sha256:6a16b69b93187244e0548cbfd25b3e4a6f9a7a2ad784625c3bec2b8d27b81aaa", size = 99506, upload-time = "2026-09-26T20:29:06.054Z" }, ] [[package]] name = "nodeenv" -version = "1.10.0" +version = "1.11.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/105de02c1322cfada6d9710d9146ef8026419d433c9d08359a2d35805811/nodeenv-1.11.0.tar.gz", hash = "sha256:3ce8fe5b71d16e8af7039ca65257354100bc772965d6bc549070649e53b1b146", size = 99301, upload-time = "2026-09-26T11:29:21.367Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, + { url = "https://files.pythonhosted.org/packages/54/c8/12811c9b48fde162bb72b6f2e78fada9a247a09d7bb5be2050a5d099c77b/nodeenv-1.11.0-py2.py3-none-any.whl", hash = "sha256:edaa16e6c14d7cf395d75d4bbd5a26390f4dc06501a33b4e76282b02cc688a25", size = 34277, upload-time = "2026-09-26T11:29:19.933Z" }, ] [[package]] @@ -58,22 +58,31 @@ dependencies = [ [package.metadata] requires-dist = [ - { name = "pre-commit", specifier = ">=4.2.0,<5.0.0" }, - { name = "python-dotenv", specifier = ">=1.1.0,<2.0.0" }, + { name = "pre-commit", specifier = ">=4.2.0" }, + { name = "python-dotenv", specifier = ">=1.1.0" }, +] + +[[package]] +name = "packaging" +version = "26.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" }, ] [[package]] name = "platformdirs" -version = "4.10.0" +version = "4.13.5" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d7/47/e4501f49c178ae1d9f4a75073fda4204f52647993f075a9db4d14930e0c5/platformdirs-4.10.0.tar.gz", hash = "sha256:31e761a6a0ca04faf7353ea759bdba55652be214725111e5aac52dfa29d4bef7", size = 31224, upload-time = "2026-05-28T03:32:53.587Z" } +sdist = { url = "https://files.pythonhosted.org/packages/58/e6/d0abbb256b44e6a05748c6942511ff67db8dbb068de0c35b24bf1b395b09/platformdirs-4.13.5.tar.gz", hash = "sha256:05831adf87d392760f4b5c3a0a95579ab5a10b18b9b9bd43680dcb47544788a0", size = 62362, upload-time = "2026-10-19T01:02:11.778Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/81/e6/cd9575ac904136b3cbf7aa7ee819ef86eedb7274e46f230e94ea4342e729/platformdirs-4.10.0-py3-none-any.whl", hash = "sha256:fb516cdb12eb0d857d0cd85a7c57cea4d060bee4578d6cf5a14dfdf8cbf8784a", size = 22743, upload-time = "2026-05-28T03:32:52.175Z" }, + { url = "https://files.pythonhosted.org/packages/6c/8a/1d8cb3842909dc193a73835adedf412e32e18f8ffd59f43983949622fe6f/platformdirs-4.13.5-py3-none-any.whl", hash = "sha256:53d3ec4280b815c1e0b02edad7f9ee8fda1ba9c0865dd9c2e6e63849ce82fc1e", size = 32779, upload-time = "2026-10-19T01:02:10.17Z" }, ] [[package]] name = "pre-commit" -version = "4.6.0" +version = "4.7.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cfgv" }, @@ -82,31 +91,30 @@ dependencies = [ { name = "pyyaml" }, { name = "virtualenv" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8e/22/2de9408ac81acbb8a7d05d4cc064a152ccf33b3d480ebe0cd292153db239/pre_commit-4.6.0.tar.gz", hash = "sha256:718d2208cef53fdc38206e40524a6d4d9576d103eb16f0fec11c875e7716e9d9", size = 198525, upload-time = "2026-04-21T20:31:41.613Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/c3/6965c2690ddce885ec75121572d065a5b94c5a5d8d3e657927ecda9fd715/pre_commit-4.7.0.tar.gz", hash = "sha256:1f3deee12b914c8fa184bd2b5953391c2e4f36763e403d4a23dc9983fa7c6e86", size = 198120, upload-time = "2026-10-12T20:45:40.32Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl", hash = "sha256:e2cf246f7299edcabcf15f9b0571fdce06058527f0a06535068a86d38089f29b", size = 226472, upload-time = "2026-04-21T20:31:40.092Z" }, + { url = "https://files.pythonhosted.org/packages/eb/83/ce0a196e4f7b20b11ce3aa1fc72fb973f64b7628d51c1b8aebad37fb7f81/pre_commit-4.7.0-py2.py3-none-any.whl", hash = "sha256:2e229038ad3656081b70c27913157e4b636ef306f643f024a8a8e3f0f7b2f877", size = 226261, upload-time = "2026-10-12T20:45:38.858Z" }, ] [[package]] name = "python-discovery" -version = "1.4.2" +version = "1.6.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "filelock" }, - { name = "platformdirs" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0b/1a/cbbaf13b730abb0a16b964d984e19f2fe520c21a4dc664051359a3f5a9e7/python_discovery-1.4.2.tar.gz", hash = "sha256:8f3746c4b4968d22afbb97d36e1a0e5b66e6c0f297290f2e95f05b9b8bf18690", size = 70277, upload-time = "2026-06-11T16:10:42.383Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/ee/cf608372c88ce2c136ee13c69ab6f667dbc0d0c1176af188b4ddc8853e66/python_discovery-1.6.4.tar.gz", hash = "sha256:bc70d31f0ec5de11eda13d4d64c16e24b3506a65984684f2b6c98cb83b2ea9df", size = 89883, upload-time = "2026-10-19T01:45:14.855Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1a/82/a70006589557f267f15bd384c0642ad49f0d97b690c3a05b166b9dcbad3b/python_discovery-1.4.2-py3-none-any.whl", hash = "sha256:475803f53b7b2ed6e490e27373f9d8340f7d2eebf9acdaf645d7d714c97bb500", size = 33886, upload-time = "2026-06-11T16:10:41.192Z" }, + { url = "https://files.pythonhosted.org/packages/60/91/1aa70d3dfcab0dbcf88a2cbc3bce7f7c69562287a90216e617eaa4937e25/python_discovery-1.6.4-py3-none-any.whl", hash = "sha256:530179cc51a1816d512b15635b3dc931295ae12efb183783ad31190a5d425d2b", size = 39385, upload-time = "2026-10-19T01:45:13.668Z" }, ] [[package]] name = "python-dotenv" -version = "1.2.2" +version = "1.2.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } +sdist = { url = "https://files.pythonhosted.org/packages/74/26/2fbeedb218a787a5eea551c7532cac4e009f83d689dd2faa0d0353473f86/python_dotenv-1.2.4.tar.gz", hash = "sha256:f0d53e69935a851c0dcc78f3ab7aaccd8cabef0b92382b576b824212902873c0", size = 60824, upload-time = "2026-10-01T05:36:10Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, + { url = "https://files.pythonhosted.org/packages/60/d1/38f3a3405989a89ac18390803e70c6ad7c7760da4f9b83cbeca0c44a0c72/python_dotenv-1.2.4-py3-none-any.whl", hash = "sha256:42269a8a5b3fd54ffa6f3d84b18abed50064717576b4ecf03dc4a55d8aa04fdc", size = 23266, upload-time = "2026-10-01T05:36:08.633Z" }, ] [[package]] @@ -115,44 +123,6 @@ version = "6.0.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, - { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, - { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, - { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, - { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, - { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, - { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, - { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, - { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, - { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, - { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, - { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, - { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, - { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, - { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, - { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, - { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, - { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, - { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, - { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, - { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, - { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, - { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, - { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, - { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, - { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, - { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, - { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, - { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, - { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, - { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, - { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, - { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, - { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, - { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, - { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, - { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, - { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, @@ -173,27 +143,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, ] -[[package]] -name = "typing-extensions" -version = "4.15.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, -] - [[package]] name = "virtualenv" -version = "21.5.1" +version = "21.14.13" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "distlib" }, { name = "filelock" }, + { name = "packaging" }, { name = "platformdirs" }, { name = "python-discovery" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f1/a5/81f987504738e6defeed61ec1c47e2aefab3c35d8eeb87e1b3f38cf28254/virtualenv-21.5.1.tar.gz", hash = "sha256:dca3bf98275a59c652b69d68e73433e597d977c2da9198882479d1a7188009c8", size = 4578798, upload-time = "2026-06-16T16:23:58.603Z" } +sdist = { url = "https://files.pythonhosted.org/packages/70/aa/bff50daee0488e16d611258a9b6c5a1d0c9e128f8ca424e9f4e1c0444ef2/virtualenv-21.14.13.tar.gz", hash = "sha256:de5307ba4b9a0001d9c8ea02832d38f63ca432880f416c1bface61c4323fd905", size = 5570301, upload-time = "2026-10-22T20:10:16.952Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/02/3623e6169bed617ed1e2d372f7c69f92ec28d54c4dfc997055c8578ec148/virtualenv-21.5.1-py3-none-any.whl", hash = "sha256:55aa670b67bbfb991b03fda39bd3276d92c419d702376e98c5df1c9989a26783", size = 4558820, upload-time = "2026-06-16T16:23:56.963Z" }, + { url = "https://files.pythonhosted.org/packages/bb/a9/c10d8a44a29fb94a18b43fdf11f38caca53ae7c9406f11a72b1cb8f624f1/virtualenv-21.14.13-py3-none-any.whl", hash = "sha256:5fbb04138a4861580b78ba9110d529a26bc48fcc93c8ec36bc0202eeed96a788", size = 5557094, upload-time = "2026-10-22T20:10:15.096Z" }, ]