From 933cc46776aa17fadef2bc1d6253813c1cb9d5a8 Mon Sep 17 00:00:00 2001 From: CameleoGrey Date: Fri, 18 Jul 2025 20:28:04 +0300 Subject: [PATCH 1/4] v0.3.3 Achieved correct results by greynet (TODO: fix node state management to use true incremental scoring without reinserting/retracting all facts on each delta update) --- .../greynet/greynet_example_aggregations.py | 75 + .../greynet/greynet_example_all_collectors.py | 308 +++ .../greynet/greynet_example_blom_filter.py | 63 + .../greynet/greynet_example_composite_keys.py | 305 +++ examples/greynet/greynet_example_core.py | 204 ++ .../greynet/greynet_example_node_sharing.py | 132 ++ examples/greynet/greynet_example_stress_1.py | 211 +++ examples/greynet/greynet_example_stress_2.py | 364 ++++ .../greynet/greynet_example_subqueries.py | 212 +++ .../greynet/greynet_example_temporal_1.py | 225 +++ .../greynet/greynet_example_temporal_2.py | 177 ++ .../nqueens/cotwin/CotQueen.py | 7 +- .../persistence/CotwinBuilderNQueens.py | 14 +- .../score/GreynetScoreCalculatorNQueens.py | 35 + .../nqueens/scripts/solve_nqueens.py | 53 - .../nqueens/scripts/solve_nqueens_fast.py | 57 + .../solve_nqueens_greynet_experimental.py | 60 + greyjack/Cargo.lock | 2 +- greyjack/Cargo.toml | 12 +- greyjack/greyjack/agents/TabuSearch.py | 7 +- greyjack/greyjack/agents/base/Agent.py | 53 +- .../score_calculation/greynet/__init__.py | 0 .../score_calculation/greynet/builder.py | 265 +++ .../greynet/collectors/__init__.py | 0 .../greynet/collectors/avg_collector.py | 25 + .../greynet/collectors/base_collector.py | 27 + .../greynet/collectors/composite_collector.py | 63 + .../collectors/constraint_match_collector.py | 15 + .../greynet/collectors/count_collector.py | 18 + .../greynet/collectors/distinct_collector.py | 35 + .../greynet/collectors/filtering_collector.py | 35 + .../greynet/collectors/list_collector.py | 27 + .../greynet/collectors/mapping_collector.py | 32 + .../greynet/collectors/max_collector.py | 39 + .../greynet/collectors/min_collector.py | 42 + .../greynet/collectors/set_collector.py | 35 + .../greynet/collectors/stddev_collector.py | 36 + .../greynet/collectors/sum_collector.py | 23 + .../greynet/collectors/temporal_collectors.py | 180 ++ .../greynet/collectors/variance_collector.py | 43 + .../greynet/common/__init__.py | 0 .../greynet/common/index/__init__.py | 0 .../greynet/common/index/advanced_index.py | 173 ++ .../greynet/common/index/uni_index.py | 24 + .../greynet/common/index_properties.py | 7 + .../greynet/common/joiner_type.py | 48 + .../score_calculation/greynet/constraint.py | 20 + .../greynet/constraint_factory.py | 56 + .../greynet/constraint_tools/__init__.py | 0 .../connected_range_tracker.py | 105 ++ .../constraint_tools/consecutive_set_tree.py | 134 ++ .../constraint_tools/counting_bloom_filter.py | 124 ++ .../greynet/constraint_weights.py | 29 + .../greynet/core/__init__.py | 0 .../greynet/core/scheduler.py | 14 + .../score_calculation/greynet/core/tuple.py | 103 ++ .../greynet/core/tuple_pool.py | 66 + .../greynet/docs/manual_1.md | 628 +++++++ .../greynet/docs/manual_2.md | 885 +++++++++ .../greynet/function/__init__.py | 11 + .../greynet/function/bi_function.py | 7 + .../greynet/function/bi_predicate.py | 7 + .../greynet/function/function.py | 7 + .../greynet/function/penta_predicate.py | 7 + .../greynet/function/predicate.py | 7 + .../greynet/function/quad_function.py | 7 + .../greynet/function/quad_predicate.py | 7 + .../greynet/function/supplier.py | 9 + .../greynet/function/tri_function.py | 7 + .../greynet/function/tri_predicate.py | 7 + .../score_calculation/greynet/greynet_fact.py | 27 + .../greynet/nodes/__init__.py | 0 .../greynet/nodes/abstract_node.py | 40 + .../greynet/nodes/alpha_node.py | 34 + .../greynet/nodes/base_join_node.py | 100 + .../greynet/nodes/beta_node.py | 87 + .../greynet/nodes/conditional_node.py | 70 + .../greynet/nodes/filter_node.py | 23 + .../greynet/nodes/flatmap_node.py | 47 + .../greynet/nodes/from_uni_node.py | 29 + .../greynet/nodes/group_node.py | 65 + .../greynet/nodes/join_node.py | 25 + .../greynet/nodes/scoring_node.py | 60 + .../greynet/nodes/sequence_pattern_node.py | 164 ++ .../greynet/nodes/sliding_window_node.py | 120 ++ .../greynet/optimization/__init__.py | 0 .../greynet/optimization/batch_processor.py | 39 + .../greynet/optimization/node_sharing.py | 41 + .../score_calculation/greynet/session.py | 122 ++ .../greynet/streams/__init__.py | 0 .../greynet/streams/abstract_stream.py | 49 + .../greynet/streams/join_adapters.py | 86 + .../greynet/streams/scoring_stream.py | 24 + .../greynet/streams/stream.py | 193 ++ .../greynet/streams/stream_definition.py | 281 +++ .../score_calculation/greynet/tuple_tools.py | 57 + .../GreynetScoreCalculator.py | 251 +++ .../score_requesters/OOPScoreRequester.py | 163 +- greyjack/pyproject.toml | 9 +- .../concrete_tabu_search_base_macros.rs | 18 +- .../scores/hard_medium_soft_score.rs | 9 + .../scores/hard_soft_score.rs | 5 + .../score_calculation/scores/simple_score.rs | 5 + greyjack/uv.lock | 1647 +++++++++++++++++ 104 files changed, 9781 insertions(+), 124 deletions(-) create mode 100644 examples/greynet/greynet_example_aggregations.py create mode 100644 examples/greynet/greynet_example_all_collectors.py create mode 100644 examples/greynet/greynet_example_blom_filter.py create mode 100644 examples/greynet/greynet_example_composite_keys.py create mode 100644 examples/greynet/greynet_example_core.py create mode 100644 examples/greynet/greynet_example_node_sharing.py create mode 100644 examples/greynet/greynet_example_stress_1.py create mode 100644 examples/greynet/greynet_example_stress_2.py create mode 100644 examples/greynet/greynet_example_subqueries.py create mode 100644 examples/greynet/greynet_example_temporal_1.py create mode 100644 examples/greynet/greynet_example_temporal_2.py create mode 100644 examples/object_oriented/nqueens/score/GreynetScoreCalculatorNQueens.py delete mode 100644 examples/object_oriented/nqueens/scripts/solve_nqueens.py create mode 100644 examples/object_oriented/nqueens/scripts/solve_nqueens_fast.py create mode 100644 examples/object_oriented/nqueens/scripts/solve_nqueens_greynet_experimental.py create mode 100644 greyjack/greyjack/score_calculation/greynet/__init__.py create mode 100644 greyjack/greyjack/score_calculation/greynet/builder.py create mode 100644 greyjack/greyjack/score_calculation/greynet/collectors/__init__.py create mode 100644 greyjack/greyjack/score_calculation/greynet/collectors/avg_collector.py create mode 100644 greyjack/greyjack/score_calculation/greynet/collectors/base_collector.py create mode 100644 greyjack/greyjack/score_calculation/greynet/collectors/composite_collector.py create mode 100644 greyjack/greyjack/score_calculation/greynet/collectors/constraint_match_collector.py create mode 100644 greyjack/greyjack/score_calculation/greynet/collectors/count_collector.py create mode 100644 greyjack/greyjack/score_calculation/greynet/collectors/distinct_collector.py create mode 100644 greyjack/greyjack/score_calculation/greynet/collectors/filtering_collector.py create mode 100644 greyjack/greyjack/score_calculation/greynet/collectors/list_collector.py create mode 100644 greyjack/greyjack/score_calculation/greynet/collectors/mapping_collector.py create mode 100644 greyjack/greyjack/score_calculation/greynet/collectors/max_collector.py create mode 100644 greyjack/greyjack/score_calculation/greynet/collectors/min_collector.py create mode 100644 greyjack/greyjack/score_calculation/greynet/collectors/set_collector.py create mode 100644 greyjack/greyjack/score_calculation/greynet/collectors/stddev_collector.py create mode 100644 greyjack/greyjack/score_calculation/greynet/collectors/sum_collector.py create mode 100644 greyjack/greyjack/score_calculation/greynet/collectors/temporal_collectors.py create mode 100644 greyjack/greyjack/score_calculation/greynet/collectors/variance_collector.py create mode 100644 greyjack/greyjack/score_calculation/greynet/common/__init__.py create mode 100644 greyjack/greyjack/score_calculation/greynet/common/index/__init__.py create mode 100644 greyjack/greyjack/score_calculation/greynet/common/index/advanced_index.py create mode 100644 greyjack/greyjack/score_calculation/greynet/common/index/uni_index.py create mode 100644 greyjack/greyjack/score_calculation/greynet/common/index_properties.py create mode 100644 greyjack/greyjack/score_calculation/greynet/common/joiner_type.py create mode 100644 greyjack/greyjack/score_calculation/greynet/constraint.py create mode 100644 greyjack/greyjack/score_calculation/greynet/constraint_factory.py create mode 100644 greyjack/greyjack/score_calculation/greynet/constraint_tools/__init__.py create mode 100644 greyjack/greyjack/score_calculation/greynet/constraint_tools/connected_range_tracker.py create mode 100644 greyjack/greyjack/score_calculation/greynet/constraint_tools/consecutive_set_tree.py create mode 100644 greyjack/greyjack/score_calculation/greynet/constraint_tools/counting_bloom_filter.py create mode 100644 greyjack/greyjack/score_calculation/greynet/constraint_weights.py create mode 100644 greyjack/greyjack/score_calculation/greynet/core/__init__.py create mode 100644 greyjack/greyjack/score_calculation/greynet/core/scheduler.py create mode 100644 greyjack/greyjack/score_calculation/greynet/core/tuple.py create mode 100644 greyjack/greyjack/score_calculation/greynet/core/tuple_pool.py create mode 100644 greyjack/greyjack/score_calculation/greynet/docs/manual_1.md create mode 100644 greyjack/greyjack/score_calculation/greynet/docs/manual_2.md create mode 100644 greyjack/greyjack/score_calculation/greynet/function/__init__.py create mode 100644 greyjack/greyjack/score_calculation/greynet/function/bi_function.py create mode 100644 greyjack/greyjack/score_calculation/greynet/function/bi_predicate.py create mode 100644 greyjack/greyjack/score_calculation/greynet/function/function.py create mode 100644 greyjack/greyjack/score_calculation/greynet/function/penta_predicate.py create mode 100644 greyjack/greyjack/score_calculation/greynet/function/predicate.py create mode 100644 greyjack/greyjack/score_calculation/greynet/function/quad_function.py create mode 100644 greyjack/greyjack/score_calculation/greynet/function/quad_predicate.py create mode 100644 greyjack/greyjack/score_calculation/greynet/function/supplier.py create mode 100644 greyjack/greyjack/score_calculation/greynet/function/tri_function.py create mode 100644 greyjack/greyjack/score_calculation/greynet/function/tri_predicate.py create mode 100644 greyjack/greyjack/score_calculation/greynet/greynet_fact.py create mode 100644 greyjack/greyjack/score_calculation/greynet/nodes/__init__.py create mode 100644 greyjack/greyjack/score_calculation/greynet/nodes/abstract_node.py create mode 100644 greyjack/greyjack/score_calculation/greynet/nodes/alpha_node.py create mode 100644 greyjack/greyjack/score_calculation/greynet/nodes/base_join_node.py create mode 100644 greyjack/greyjack/score_calculation/greynet/nodes/beta_node.py create mode 100644 greyjack/greyjack/score_calculation/greynet/nodes/conditional_node.py create mode 100644 greyjack/greyjack/score_calculation/greynet/nodes/filter_node.py create mode 100644 greyjack/greyjack/score_calculation/greynet/nodes/flatmap_node.py create mode 100644 greyjack/greyjack/score_calculation/greynet/nodes/from_uni_node.py create mode 100644 greyjack/greyjack/score_calculation/greynet/nodes/group_node.py create mode 100644 greyjack/greyjack/score_calculation/greynet/nodes/join_node.py create mode 100644 greyjack/greyjack/score_calculation/greynet/nodes/scoring_node.py create mode 100644 greyjack/greyjack/score_calculation/greynet/nodes/sequence_pattern_node.py create mode 100644 greyjack/greyjack/score_calculation/greynet/nodes/sliding_window_node.py create mode 100644 greyjack/greyjack/score_calculation/greynet/optimization/__init__.py create mode 100644 greyjack/greyjack/score_calculation/greynet/optimization/batch_processor.py create mode 100644 greyjack/greyjack/score_calculation/greynet/optimization/node_sharing.py create mode 100644 greyjack/greyjack/score_calculation/greynet/session.py create mode 100644 greyjack/greyjack/score_calculation/greynet/streams/__init__.py create mode 100644 greyjack/greyjack/score_calculation/greynet/streams/abstract_stream.py create mode 100644 greyjack/greyjack/score_calculation/greynet/streams/join_adapters.py create mode 100644 greyjack/greyjack/score_calculation/greynet/streams/scoring_stream.py create mode 100644 greyjack/greyjack/score_calculation/greynet/streams/stream.py create mode 100644 greyjack/greyjack/score_calculation/greynet/streams/stream_definition.py create mode 100644 greyjack/greyjack/score_calculation/greynet/tuple_tools.py create mode 100644 greyjack/greyjack/score_calculation/score_calculators/GreynetScoreCalculator.py create mode 100644 greyjack/uv.lock diff --git a/examples/greynet/greynet_example_aggregations.py b/examples/greynet/greynet_example_aggregations.py new file mode 100644 index 0000000..8fd940c --- /dev/null +++ b/examples/greynet/greynet_example_aggregations.py @@ -0,0 +1,75 @@ +# In main_example_fixed.py, after the existing code + +# --- 4. New Usage Example for Aggregations --- +from dataclasses import dataclass +from greyjack.score_calculation.greynet.greynet_fact import * +from greyjack.score_calculation.greynet.builder import ConstraintBuilder, Collectors +from greyjack.score_calculation.scores.SimpleScore import SimpleScore + +@greynet_fact +@dataclass() +class SalesTransaction: + region: str + amount: float + +builder = ConstraintBuilder(name="temporal-security", score_class=SimpleScore) +# Define a constraint to analyze sales data per region. +# We use a penalty of 0 because the goal is data extraction, not scoring. +@builder.constraint("Sales Regional Analysis") +def sales_analysis(): + return ( + builder.for_each(SalesTransaction) + .group_by( + lambda tx: tx.region, + Collectors.compose({ + "min_sale": Collectors.min(lambda tx: tx.amount), + "max_sale": Collectors.max(lambda tx: tx.amount), + "avg_sale": Collectors.avg(lambda tx: tx.amount), + "stddev_sale": Collectors.stddev(lambda tx: tx.amount), + "total_sales": Collectors.sum(lambda tx: tx.amount), + "num_sales": Collectors.count() + }) + ) + .penalize_simple(0) + ) + +# --- Execute the new analysis --- +print("\n" + "="*50) +print("--- Starting Sales Aggregation Example ---") +print("="*50) + +# Re-use the same builder to create a new session if needed, or add to the existing one. +# For a clean test, we'll build it again. +sales_session = builder.build() + +transactions = [ + SalesTransaction("North", 110.0), + SalesTransaction("North", 150.0), + SalesTransaction("North", 195.5), + SalesTransaction("South", 500.0), + SalesTransaction("South", 600.0), + SalesTransaction("West", 300.0), +] + +sales_session.insert_batch(transactions) +sales_session.flush() + +print("\n--- Sales Aggregation Results ---") +matches = sales_session.get_constraint_matches() +for constraint_id, violations in matches.items(): + if constraint_id == "Sales Regional Analysis": + print(f"\nAnalysis: '{constraint_id}'") + # Sort results by region for consistent output + sorted_violations = sorted(violations, key=lambda v: v[1].fact_a) + for _, facts_tuple in sorted_violations: + region = facts_tuple.fact_a + stats = facts_tuple.fact_b + print(f" - Region: {region}") + print(f" - Count: {stats['num_sales']}") + print(f" - Total Sales: ${stats['total_sales']:.2f}") + print(f" - Average Sale: ${stats['avg_sale']:.2f}") + print(f" - Min Sale: ${stats['min_sale']:.2f}") + print(f" - Max Sale: ${stats['max_sale']:.2f}") + print(f" - Std Dev: ${stats['stddev_sale']:.2f}") + +print("\n--- Sales Example Complete ---") diff --git a/examples/greynet/greynet_example_all_collectors.py b/examples/greynet/greynet_example_all_collectors.py new file mode 100644 index 0000000..e9a210e --- /dev/null +++ b/examples/greynet/greynet_example_all_collectors.py @@ -0,0 +1,308 @@ +# main_example.py + +from __future__ import annotations +from dataclasses import dataclass +from typing import Type, Callable, List +from datetime import datetime, timedelta, timezone +from greyjack.score_calculation.greynet.greynet_fact import * +from greyjack.score_calculation.greynet.builder import ConstraintBuilder, Collectors +from greyjack.score_calculation.scores.SimpleScore import SimpleScore + + +# 1. Data Class Definitions (Facts) +# ================================= + +@greynet_fact +@dataclass() +class Sale: + sale_id: str + product_id: str + customer_id: str + price: float + quantity: int + timestamp: datetime + +@greynet_fact +@dataclass() +class Shipment: + order_id: str + shipment_id: str + shipment_no: int + +@greynet_fact +@dataclass() +class Maintenance: + machine_id: str + start_time: datetime + end_time: datetime + +@greynet_fact +@dataclass() +class UserEvent: + user_id: str + event_type: str + value: float # e.g., transaction amount + timestamp: datetime + + +# 2. Constraint and Collector Definitions +# ======================================= + +# Initialize the constraint builder +cb = ConstraintBuilder(name="collector_showcase", score_class=SimpleScore) + +@cb.constraint("count_total_sales_per_product") +def count_collector_example(): + """Demonstrates: CountCollector + Counts the number of sales transactions for each product. Penalizes if a product has more than 3 sales. + """ + return (cb.for_each(Sale) + .group_by(lambda s: s.product_id, Collectors.count()) + .filter(lambda product_id, count: count > 3) + .penalize_simple(lambda product_id, count: count) + ) + +@cb.constraint("sum_revenue_per_product") +def sum_collector_example(): + """Demonstrates: SumCollector + Calculates the total revenue (price * quantity) for each product. + """ + return (cb.for_each(Sale) + .group_by(lambda s: s.product_id, Collectors.sum(lambda s: s.price * s.quantity)) + .filter(lambda product_id, total_revenue: total_revenue > 0) + .penalize_simple(lambda product_id, total_revenue: 0) # Use penalty 0 to just report + ) + +@cb.constraint("basic_price_stats_per_product") +def min_max_avg_collectors_example(): + """Demonstrates: MinCollector, MaxCollector, AvgCollector + Finds the minimum, maximum, and average sale price for each product. + """ + return (cb.for_each(Sale) + .group_by(lambda s: s.product_id, Collectors.compose({ + "min_price": Collectors.min(lambda s: s.price), + "max_price": Collectors.max(lambda s: s.price), + "avg_price": Collectors.avg(lambda s: s.price) + })) + .filter(lambda product_id, stats: stats["max_price"] > 1.0) + .penalize_simple(lambda product_id, stats: 0) # Reporting only + ) + +@cb.constraint("advanced_price_stats_per_product") +def stddev_variance_collectors_example(): + """Demonstrates: StdDevCollector, VarianceCollector + Calculates the standard deviation and variance of prices for each product. + """ + return (cb.for_each(Sale) + .group_by(lambda s: s.product_id, Collectors.compose({ + "price_stddev": Collectors.stddev(lambda s: s.price), + "price_variance": Collectors.variance(lambda s: s.price) + })) + .filter(lambda product_id, stats: stats["price_stddev"] > 0) + .penalize_simple(lambda product_id, stats: 0) # Reporting only + ) + +@cb.constraint("list_of_sales_per_product") +def list_collector_example(): + """Demonstrates: ListCollector + Collects all `Sale` objects for each product into a list. + """ + return (cb.for_each(Sale) + .group_by(lambda s: s.product_id, Collectors.to_list()) + .filter(lambda product_id, sales_list: len(sales_list) > 0) + .penalize_simple(lambda product_id, sales_list: 0) # Reporting only + ) + +@cb.constraint("set_of_customers_per_product") +def set_collector_example(): + """Demonstrates: SetCollector and MappingCollector + Collects the unique set of customer IDs for each product. + """ + return (cb.for_each(Sale) + .group_by( + lambda s: s.product_id, + Collectors.mapping( + lambda s: s.customer_id, + Collectors.to_set() + ) + ) + .filter(lambda product_id, customer_set: len(customer_set) > 0) + .penalize_simple(lambda product_id, customer_set: 0) # Reporting only + ) + +@cb.constraint("distinct_list_of_customers_per_product") +def distinct_collector_example(): + """Demonstrates: DistinctCollector + Collects a list of unique customer IDs for each product, preserving insertion order. + """ + return (cb.for_each(Sale) + .group_by( + lambda s: s.product_id, + Collectors.mapping( + lambda s: s.customer_id, + Collectors.distinct() + ) + ) + .filter(lambda product_id, customer_list: len(customer_list) > 0) + .penalize_simple(lambda product_id, customer_list: 0) # Reporting only + ) + +@cb.constraint("count_high_quantity_sales") +def filtering_collector_example(): + """Demonstrates: FilteringCollector + Counts only the sales where the quantity is greater than 2. + """ + return (cb.for_each(Sale) + .group_by( + lambda s: s.product_id, + Collectors.filtering( + lambda s: s.quantity > 2, + Collectors.count() + ) + ) + .filter(lambda product_id, count: count > 0) + .penalize_simple(lambda product_id, count: 0) # Reporting only + ) + +@cb.constraint("find_consecutive_shipments") +def consecutive_sequences_collector_example(): + """Demonstrates: consecutive_sequences + Finds consecutive sequences of shipment numbers for each order. + """ + return (cb.for_each(Shipment) + .group_by( + lambda s: s.order_id, + Collectors.consecutive_sequences(lambda s: s.shipment_no) + ) + .filter(lambda order_id, sequences: any(seq.length > 1 for seq in sequences)) + .penalize_simple(lambda order_id, sequences: 0) # Reporting only + ) + +@cb.constraint("find_overlapping_maintenance") +def connected_ranges_collector_example(): + """Demonstrates: connected_ranges + Finds groups of overlapping or adjacent maintenance windows for each machine. + """ + return (cb.for_each(Maintenance) + .group_by( + lambda m: m.machine_id, + Collectors.connected_ranges( + start_func=lambda m: m.start_time, + end_func=lambda m: m.end_time + ) + ) + .filter(lambda machine_id, ranges: any(len(r.data) > 1 for r in ranges)) + .penalize_simple(lambda machine_id, ranges: 0) # Reporting only + ) + +@cb.constraint("tumbling_window_events") +def tumbling_window_example(): + """Demonstrates: TumblingWindowCollector for aggregation + Groups events into 10-second, non-overlapping ("tumbling") windows + and calculates the average transaction value for each window. + """ + # Define a key function to map timestamps to a 10-second window start time + def get_window_key(timestamp: datetime) -> datetime: + epoch = datetime(1970, 1, 1, tzinfo=timezone.utc) + window_size_sec = 10 + elapsed_sec = (timestamp - epoch).total_seconds() + window_index = int(elapsed_sec // window_size_sec) + window_start_ts = epoch.timestamp() + window_index * window_size_sec + return datetime.fromtimestamp(window_start_ts, tz=timezone.utc) + + return (cb.for_each(UserEvent) + .group_by( + group_key_function=lambda e: get_window_key(e.timestamp), + collector_supplier=Collectors.avg(lambda e: e.value) + ) + .filter(lambda window_start, avg_value: avg_value > 0) + .penalize_simple(lambda window_start, avg_value: 0) # Reporting only + ) + +# 3. Main Execution Block +# ======================= + +def run_demonstration(): + """Builds the session, inserts data, and prints the results.""" + + # --- Sample Data --- + now = datetime.now(timezone.utc) + sales_data = [ + Sale("s1", "prod-a", "cust-1", 10.0, 1, now), + Sale("s2", "prod-b", "cust-1", 25.5, 2, now + timedelta(seconds=1)), + Sale("s3", "prod-a", "cust-2", 12.0, 5, now + timedelta(seconds=2)), + Sale("s4", "prod-a", "cust-1", 11.5, 2, now + timedelta(seconds=3)), + Sale("s5", "prod-b", "cust-3", 24.0, 1, now + timedelta(seconds=4)), + Sale("s6", "prod-a", "cust-3", 12.5, 3, now + timedelta(seconds=5)), + ] + + shipments_data = [ + Shipment("order-1", "sh-101", 1), + Shipment("order-1", "sh-102", 2), + Shipment("order-2", "sh-201", 1), + Shipment("order-1", "sh-104", 4), # Gap in sequence + Shipment("order-1", "sh-103", 3), + ] + + maintenance_data = [ + Maintenance("m1", now, now + timedelta(hours=2)), + Maintenance("m2", now, now + timedelta(hours=1)), + Maintenance("m1", now + timedelta(hours=1), now + timedelta(hours=3)), # Overlaps with the first + Maintenance("m1", now + timedelta(hours=4), now + timedelta(hours=5)), # Adjacent + ] + + user_events_data = [ + UserEvent("u1", "tx", 100, now), + UserEvent("u2", "tx", 150, now + timedelta(seconds=2)), + UserEvent("u1", "tx", 50, now + timedelta(seconds=8)), + UserEvent("u3", "tx", 200, now + timedelta(seconds=11)), # New window + UserEvent("u2", "tx", 300, now + timedelta(seconds=15)), + ] + + # --- Build and Run Session --- + session = cb.build() + + print("## [INITIAL STATE] Inserting all facts...") + session.insert_batch(sales_data) + session.insert_batch(shipments_data) + session.insert_batch(maintenance_data) + session.insert_batch(user_events_data) + + matches = session.get_constraint_matches() + print_results(matches) + + # --- Demonstrate Retraction --- + print("\n\n## [RETRACTION] Retracting one sale (s6) and one shipment (sh-103)...") + sale_to_retract = sales_data[-1] # Sale("s6", "prod-a", ...) + shipment_to_retract = shipments_data[-1] # Shipment("order-1", "sh-103", 3) + + session.retract(sale_to_retract) + session.retract(shipment_to_retract) + + matches_after_retract = session.get_constraint_matches() + print("## Results after retraction:") + print_results(matches_after_retract) + +def print_results(matches): + """Helper function to print constraint matches in a structured way.""" + if not matches: + print(" No constraint matches found.") + return + + for constraint_id, match_list in matches.items(): + print(f"\n### Constraint: `{constraint_id}`") + print("-" * (len(constraint_id) + 18)) + for score_obj, match_tuple in match_list: + facts = [f for f in [ + getattr(match_tuple, 'fact_a', None), + getattr(match_tuple, 'fact_b', None), + ] if f is not None] + + print(f" - Match: {facts}") + print(f" Score: {score_obj}") + print("-" * (len(constraint_id) + 18)) + + +if __name__ == "__main__": + run_demonstration() + diff --git a/examples/greynet/greynet_example_blom_filter.py b/examples/greynet/greynet_example_blom_filter.py new file mode 100644 index 0000000..aecc4a0 --- /dev/null +++ b/examples/greynet/greynet_example_blom_filter.py @@ -0,0 +1,63 @@ +from dataclasses import dataclass +from datetime import datetime + +from greyjack.score_calculation.greynet.greynet_fact import * +from greyjack.score_calculation.greynet.builder import ConstraintBuilder, Collectors +from greyjack.score_calculation.greynet.common.joiner_type import JoinerType +from greyjack.score_calculation.scores.SimpleScore import SimpleScore +from datetime import datetime, timedelta + +@greynet_fact +@dataclass() +class UserLogin: + user_id: str + timestamp: datetime + +@greynet_fact +@dataclass() +class UserBan: + user_id: str + banned_at: datetime + +builder = ConstraintBuilder("bloom_example", score_class=SimpleScore) + +@builder.constraint("login_not_banned") +def login_not_banned(): + # Start from UserLogin facts + login_stream = builder.for_each(UserLogin) + # Join with UserBan facts, using NOT_EQUAL on user_id + # This will use the CountingBloomFilter under the hood + not_banned_stream = login_stream.join( + builder.for_each(UserBan), + JoinerType.NOT_EQUAL, + left_key_func=lambda login: login.user_id, + right_key_func=lambda ban: ban.user_id + ) + # For every login where there is NO ban with the same user_id, penalize (or reward) + return not_banned_stream.penalize_simple(lambda login, ban: -1.0) + +session = builder.build() + +# Insert facts +logins = [ + UserLogin(user_id="alice", timestamp=datetime.now()), + UserLogin(user_id="bob", timestamp=datetime.now()), + UserLogin(user_id="carol", timestamp=datetime.now()), +] + +bans = [ + UserBan(user_id="bob", banned_at=datetime.now() - timedelta(days=1)), +] + +for login in logins: + session.insert(login) +for ban in bans: + session.insert(ban) + +# Get the score (should only penalize logins by users NOT banned) +score = session.get_score() +print("Total score:", score.simple_value) + +# Get detailed constraint matches +matches = session.get_constraint_matches() +print("Constraint matches:", matches) diff --git a/examples/greynet/greynet_example_composite_keys.py b/examples/greynet/greynet_example_composite_keys.py new file mode 100644 index 0000000..69bf8eb --- /dev/null +++ b/examples/greynet/greynet_example_composite_keys.py @@ -0,0 +1,305 @@ +from __future__ import annotations +from dataclasses import dataclass, field +from typing import List, Tuple, Any +from datetime import datetime, timedelta + +# Assume the framework classes from your project are available +from greyjack.score_calculation.greynet.greynet_fact import * +from greyjack.score_calculation.greynet.builder import ConstraintBuilder, Collectors +from greyjack.score_calculation.greynet.common.joiner_type import JoinerType +from greyjack.score_calculation.scores.SimpleScore import SimpleScore +from greyjack.score_calculation.greynet.session import Session + +# --- Example Fact Definitions --- +@greynet_fact +@dataclass() +class Doctor: + id: str + name: str + specialty: str # e.g., 'Cardiology', 'Surgery', 'Psychiatry' + +@greynet_fact +@dataclass() +class Room: + id: str + type: str # e.g., 'Operating Room', 'ICU', 'Consultation', 'Psych Ward' + capacity: int + +@greynet_fact +@dataclass() +class ProcedureInfo: + code: str + description: str + required_specialty: str + required_room_type: str + +@greynet_fact +@dataclass() +class Appointment: + id: str + patient_name: str + doctor_id: str + room_id: str + procedure_code: str + patient_acuity: int # Scale 1-5 (5 is highest acuity) + start_time: datetime + end_time: datetime = field(init=False) + + def __post_init__(self): + # Appointments are 1 hour long for simplicity + object.__setattr__(self, 'end_time', self.start_time + timedelta(hours=1)) + +# --- CONSTRAINT DEFINITIONS --- + +builder = ConstraintBuilder(name="hospital_schedule_validator", score_class=SimpleScore) + + +@builder.constraint("room_double_booking") +def room_double_booking(): + """ + Finds cases where two different appointments are scheduled in the same room + at the exact same time. This is a classic composite key equality join. + The composite key is (room_id, start_time). + """ + appointments = builder.for_each(Appointment) + return ( + appointments.join( + appointments, + joiner_type=JoinerType.EQUAL, + # The composite key is a tuple of the fields we want to match on. + left_key_func=lambda app: (app.room_id, app.start_time), + right_key_func=lambda app: (app.room_id, app.start_time) + ) + # Filter to avoid matching an appointment with itself and to report each pair only once. + .filter(lambda app1, app2: app1.id < app2.id) + .penalize_simple(25) # High penalty for a critical error + ) + + +@builder.constraint("resource_allocation_mismatch") +def resource_allocation_mismatch(): + """ + Finds appointments where the doctor or room is misaligned with procedure requirements. + This demonstrates a multi-stage join process, followed by filtering. + """ + # Create streams for all our base facts + appointments = builder.for_each(Appointment) + doctors = builder.for_each(Doctor) + rooms = builder.for_each(Room) + procedures = builder.for_each(ProcedureInfo) + + # Stage 1: Join Appointments with Doctors + app_with_doc = appointments.join( + doctors, JoinerType.EQUAL, + left_key_func=lambda app: app.doctor_id, + right_key_func=lambda doc: doc.id + ) + + # Stage 2: Join the result with Rooms + app_with_doc_room = app_with_doc.join( + rooms, JoinerType.EQUAL, + left_key_func=lambda app, doc: app.room_id, + right_key_func=lambda room: room.id + ) + + # Stage 3: Join with ProcedureInfo to get the requirements for each appointment. + # This is a standard join on a primary key (the procedure code). The filtering + # for mismatches happens in the subsequent .filter() step. + final_join = app_with_doc_room.join( + procedures, JoinerType.EQUAL, + left_key_func=lambda app, doc, room: app.procedure_code, + right_key_func=lambda proc: proc.code + ) + + return ( + final_join + .filter( + lambda app, doc, room, proc: ( + doc.specialty != proc.required_specialty or + room.type != proc.required_room_type + ) + ) + .penalize_simple(10) # Heavy penalty for resource mismatch + ) + + +@builder.constraint("scheduling_priority_inversion") +def scheduling_priority_inversion(): + """ + Finds cases where a doctor handles a low-priority case before a high-priority + case on the same day. This is achieved by first grouping appointments by + doctor and day, and then filtering for priority/time inversions. + """ + appointments = builder.for_each(Appointment) + + # Define the composite key for grouping: (doctor_id, date) + def get_grouping_key(app: Appointment) -> tuple: + return (app.doctor_id, app.start_time.date()) + + return ( + appointments.join( + appointments, + joiner_type=JoinerType.EQUAL, + left_key_func=get_grouping_key, + right_key_func=get_grouping_key + ) + # Filter 1: Ensure we only consider each unique pair of appointments once. + # This prevents finding both (A, B) and (B, A) as separate issues. + .filter( + lambda app1, app2: app1.id < app2.id + ) + # Filter 2: The actual inversion logic. + # An inversion exists if the appointment with the higher acuity is scheduled + # LATER than the one with the lower acuity. + .filter( + lambda app1, app2: ( + (app1.patient_acuity > app2.patient_acuity and app1.start_time > app2.start_time) or + (app2.patient_acuity > app1.patient_acuity and app2.start_time > app1.start_time) + ) + ) + .penalize_simple(5) + ) + + + +# --- DATA POPULATION & SIMULATION EXECUTION --- + +def populate_data() -> List[Any]: + """Creates a set of carefully crafted facts to trigger our constraints.""" + + # --- Resources --- + doc_surgeon = Doctor(id="D1", name="Dr. Anya Sharma", specialty="Surgery") + doc_cardiologist = Doctor(id="D2", name="Dr. Ben Carter", specialty="Cardiology") + doc_psychiatrist = Doctor(id="D3", name="Dr. Chloe Davis", specialty="Psychiatry") + + room_or = Room(id="R1", type="Operating Room", capacity=1) + room_consult = Room(id="R2", type="Consultation", capacity=1) + room_psych = Room(id="R3", type="Psych Ward", capacity=1) + + proc_bypass = ProcedureInfo(code="PROC-001", description="Heart Bypass", required_specialty="Surgery", required_room_type="Operating Room") + proc_eval = ProcedureInfo(code="PROC-002", description="Cardiac Evaluation", required_specialty="Cardiology", required_room_type="Consultation") + proc_therapy = ProcedureInfo(code="PROC-003", description="Cognitive Therapy", required_specialty="Psychiatry", required_room_type="Psych Ward") + + # --- Schedule for a single day --- + today = datetime.now().date() + + appointments = [ + # --- Correctly Scheduled Appointments --- + Appointment(id="A1", patient_name="John Smith", doctor_id="D2", room_id="R2", procedure_code="PROC-002", patient_acuity=3, start_time=datetime.combine(today, datetime.min.time(),).replace(hour=9)), + Appointment(id="A2", patient_name="Jane Doe", doctor_id="D1", room_id="R1", procedure_code="PROC-001", patient_acuity=5, start_time=datetime.combine(today, datetime.min.time(),).replace(hour=11)), + + # --- CONSTRAINT 1 (Resource Mismatch) TRIGGERS --- + Appointment(id="A3", patient_name="Peter Jones", doctor_id="D1", room_id="R2", procedure_code="PROC-001", patient_acuity=4, start_time=datetime.combine(today, datetime.min.time(),).replace(hour=14)), + Appointment(id="A4", patient_name="Mary Williams", doctor_id="D3", room_id="R2", procedure_code="PROC-002", patient_acuity=2, start_time=datetime.combine(today, datetime.min.time(),).replace(hour=10)), + + # --- CONSTRAINT 2 (Priority Inversion) TRIGGERS --- + Appointment(id="A5", patient_name="Kevin Brown", doctor_id="D2", room_id="R2", procedure_code="PROC-002", patient_acuity=2, start_time=datetime.combine(today, datetime.min.time(),).replace(hour=13)), + Appointment(id="A6", patient_name="Laura Green", doctor_id="D2", room_id="R2", procedure_code="PROC-002", patient_acuity=5, start_time=datetime.combine(today, datetime.min.time(),).replace(hour=15)), + + # --- CONSTRAINT 3 (Double Booking) TRIGGER --- + # Appointment A7 is scheduled in the same room (R2) at the same time as A1. + Appointment(id="A7", patient_name="Susan King", doctor_id="D3", room_id="R2", procedure_code="PROC-003", patient_acuity=1, start_time=datetime.combine(today, datetime.min.time(),).replace(hour=9)), + ] + + return [doc_surgeon, doc_cardiologist, doc_psychiatrist, room_or, room_consult, room_psych, proc_bypass, proc_eval, proc_therapy] + appointments + + +if __name__ == "__main__": + + print("=" * 80) + print("RUNNING HOSPITAL SCHEDULING VALIDATOR") + print("=" * 80) + + # 1. Build the session from our constraint definitions + session = builder.build() + + # 2. Populate the session with facts + all_facts = populate_data() + session.insert_batch(all_facts) + print(f"INFO: Inserted {len(all_facts)} facts into the session.") + + # 3. Fire the rules + print("INFO: Firing all rules and evaluating constraints...") + session.flush() + print("\n") + + # 4. Analyze and report the results + print("=" * 80) + print("VALIDATION REPORT") + print("=" * 80) + + matches = session.get_constraint_matches() + + if not matches: + print("SUCCESS: No constraint violations found. The schedule is valid.") + else: + # --- Report on Double Bookings --- + if "room_double_booking" in matches: + print("\n--- VIOLATION: Room Double Booking (Penalty: 25 per issue) ---") + for i, (score, match_facts) in enumerate(matches["room_double_booking"]): + + # Unpack facts from the BiTuple object's attributes + app1, app2 = match_facts.fact_a, match_facts.fact_b + + print(f"\n Issue #{i+1}: Room '{app1.room_id}' is double-booked at {app1.start_time.strftime('%I:%M %p')}.") + print(f" - Appointment ID: {app1.id} (Patient: {app1.patient_name})") + print(f" - Appointment ID: {app2.id} (Patient: {app2.patient_name})") + + # --- Report on Resource Mismatches --- + if "resource_allocation_mismatch" in matches: + print("\n--- VIOLATION: Resource Allocation Mismatch (Penalty: 10 per issue) ---") + for i, (score, match_facts) in enumerate(matches["resource_allocation_mismatch"]): + + # This join chain results in a QuadTuple + app, doc, room, proc = match_facts.fact_a, match_facts.fact_b, match_facts.fact_c, match_facts.fact_d + + print(f"\n Issue #{i+1}: Appointment '{app.id}' for patient '{app.patient_name}'") + print(f" Procedure: '{proc.description}' ({proc.code})") + if doc.specialty != proc.required_specialty: + print(f" [X] Specialty Mismatch: Doctor '{doc.name}' is a '{doc.specialty}', but procedure requires a '{proc.required_specialty}'.") + if room.type != proc.required_room_type: + print(f" [X] Room Mismatch: Room '{room.id}' is a '{room.type}', but procedure requires a '{proc.required_room_type}'.") + + # --- Report on Priority Inversions --- + if "scheduling_priority_inversion" in matches: + print("\n--- VIOLATION: Scheduling Priority Inversion (Penalty: 5 per issue) ---") + for i, (score, match_facts) in enumerate(matches["scheduling_priority_inversion"]): + + # This self-join results in a BiTuple + high_app, low_app = match_facts.fact_a, match_facts.fact_b + + doctor_name = next((d.name for d in all_facts if isinstance(d, Doctor) and d.id == high_app.doctor_id), "Unknown") + print(f"\n Issue #{i+1}: Inefficient scheduling for Dr. {doctor_name}") + print(f" - High Acuity Patient '{high_app.patient_name}' (Acuity: {high_app.patient_acuity}) is scheduled at {high_app.start_time.strftime('%I:%M %p')}.") + print(f" - Low Acuity Patient '{low_app.patient_name}' (Acuity: {low_app.patient_acuity}) is scheduled EARLIER at {low_app.start_time.strftime('%I:%M %p')}.") + print(" [!] Recommendation: Prioritize high-acuity patients by scheduling them earlier in the day.") + + # --- Report on Priority Inversions --- + if "scheduling_priority_inversion" in matches: + print("\n--- VIOLATION: Scheduling Priority Inversion (Penalty: 5 per issue) ---") + for i, (score, match_facts) in enumerate(matches["scheduling_priority_inversion"]): + + # Unpack the generic pair from the BiTuple + app1, app2 = match_facts.fact_a, match_facts.fact_b + + # Dynamically determine which is the high-acuity and low-acuity appointment + if app1.patient_acuity > app2.patient_acuity: + high_app, low_app = app1, app2 + else: + high_app, low_app = app2, app1 + + + doctor_name = next((d.name for d in all_facts if isinstance(d, Doctor) and d.id == high_app.doctor_id), "Unknown") + print(f"\n Issue #{i+1}: Inefficient scheduling for Dr. {doctor_name}") + print(f" - High Acuity Patient '{high_app.patient_name}' (Acuity: {high_app.patient_acuity}) is scheduled at {high_app.start_time.strftime('%I:%M %p')}.") + print(f" - Low Acuity Patient '{low_app.patient_name}' (Acuity: {low_app.patient_acuity}) is scheduled EARLIER at {low_app.start_time.strftime('%I:%M %p')}.") + print(" [!] Recommendation: Prioritize high-acuity patients by scheduling them earlier in the day.") + + + # 5. Print the final score + total_score = session.get_score() + print("\n" + "=" * 80) + print(f"FINAL SCHEDULING PENALTY SCORE: {total_score.simple_value}") + print("A lower score indicates a better, more valid schedule.") + print("=" * 80) + diff --git a/examples/greynet/greynet_example_core.py b/examples/greynet/greynet_example_core.py new file mode 100644 index 0000000..5343447 --- /dev/null +++ b/examples/greynet/greynet_example_core.py @@ -0,0 +1,204 @@ +from dataclasses import dataclass, field +from datetime import date, timedelta +from typing import Set + + +# Import the new builder and the desired score class. +from greyjack.score_calculation.greynet.greynet_fact import * +from greyjack.score_calculation.greynet.builder import ConstraintBuilder, JoinerType, Collectors +from greyjack.score_calculation.scores.HardMediumSoftScore import HardMediumSoftScore + + +# --- 1. Data Models --- +# Define the "facts" that will drive the rule engine. + +@greynet_fact +@dataclass() +class Employee: + name: str + skills: Set[str] = field(default_factory=set) + unavailable_dates: Set[date] = field(default_factory=set) + +@greynet_fact +@dataclass() +class Shift: + shift_id: str + employee_name: str + shift_date: date + start_time: int + end_time: int + required_skill: str + + @property + def duration(self) -> int: + return self.end_time - self.start_time + +@greynet_fact +@dataclass() +class CompanyPolicy: + max_consecutive_work_days: int = 6 + +# --- 2. Constraint Definitions --- +# Initialize the builder with the chosen score class. +builder = ConstraintBuilder(name="advanced-scheduling", score_class=HardMediumSoftScore) + +# --- Rule 1: Skill Match (Medium Priority) --- +@builder.constraint("Required skill missing") +def required_skill_missing(): + return ( + builder.for_each(Shift) + .join( + builder.for_each(Employee), + JoinerType.EQUAL, + left_key_func=lambda shift: shift.employee_name, + right_key_func=lambda employee: employee.name + ) + .filter(lambda shift, employee: shift.required_skill not in employee.skills) + .penalize_medium(1.0) # Use new penalty method + ) + +# --- Rule 2: Employee Existence (Hard Priority) --- +@builder.constraint("Shift for non-existent employee") +def shift_for_non_existent_employee(): + return ( + builder.for_each(Shift) + .if_not_exists( + Employee, + # FIX: Removed JoinerType.EQUAL, which was an invalid argument. + left_key=lambda shift: shift.employee_name, + right_key=lambda employee: employee.name + ) + .penalize_hard(1.0) # Use new penalty method + ) + +# --- Rule 3: Availability (Medium Priority) --- +@builder.constraint("Scheduled on unavailable day") +def scheduled_on_unavailable_day(): + return ( + builder.for_each(Shift) + .join( + builder.for_each(Employee), + JoinerType.EQUAL, + left_key_func=lambda shift: shift.employee_name, + right_key_func=lambda employee: employee.name + ) + .filter(lambda shift, employee: shift.shift_date in employee.unavailable_dates) + .penalize_medium(1.0) # Use new penalty method + ) + +# --- Rule 4: Overlapping Shifts (Hard Priority) --- +@builder.constraint("Overlapping shifts") +def overlapping_shifts(): + return ( + builder.for_each(Shift) + .join( + builder.for_each(Shift), + JoinerType.EQUAL, + left_key_func=lambda s: (s.employee_name, s.shift_date), + right_key_func=lambda s: (s.employee_name, s.shift_date) + ) + .filter(lambda s1, s2: id(s1) < id(s2)) + .filter(lambda s1, s2: max(s1.start_time, s2.start_time) < min(s1.end_time, s2.end_time)) + .penalize_hard(1.0) # Use new penalty method + ) + +# --- Rule 5: Maximum Consecutive Work Days (Soft Priority) --- +@builder.constraint("Exceeds max consecutive work days") +def max_consecutive_days(): + # This penalizes based on the number of days over the limit. + penalty_func = lambda name, sequences, policy: sum( + seq.length - policy.max_consecutive_work_days + for seq in sequences if seq.length > policy.max_consecutive_work_days + ) + + return ( + builder.for_each(Shift) + .group_by( + lambda shift: shift.employee_name, + Collectors.consecutive_sequences( + sequence_func=lambda shift: shift.shift_date, + increment_func=lambda d, i: d + timedelta(days=i) + ) + ) + .join( + builder.for_each(CompanyPolicy), + JoinerType.GREATER_THAN, # A dummy join to bring the policy into the stream + left_key_func=lambda name, sequences: 1, + right_key_func=lambda policy: 0 + ) + .filter(lambda name, sequences, policy: any(seq.length > policy.max_consecutive_work_days for seq in sequences)) + .penalize_soft(lambda name, sequences, policy: penalty_func(name, sequences, policy) * 50) # Dynamic penalty + ) + + +# --- 3. Execution and Verification (Updated for new score object) --- + +print("--- Building Greynet Session ---") +session = builder.build() + +employee_ana = Employee("Ana", skills={"Cashier", "Manager"}, unavailable_dates={date(2025, 7, 18)}) +employee_ben = Employee("Ben", skills={"Chef"}) +policy = CompanyPolicy(max_consecutive_work_days=5) + +shifts = [ + Shift("S01", "Ben", date(2025, 7, 14), 9, 17, "Chef"), Shift("S02", "Ben", date(2025, 7, 15), 9, 17, "Chef"), + Shift("S03", "Ben", date(2025, 7, 16), 9, 17, "Chef"), Shift("S04", "Ben", date(2025, 7, 17), 9, 17, "Chef"), + Shift("S05", "Ben", date(2025, 7, 18), 9, 17, "Chef"), Shift("S06", "Ben", date(2025, 7, 19), 9, 17, "Chef"), + Shift("S07", "Ana", date(2025, 7, 15), 9, 18, "Cashier"), Shift("S08", "Ana", date(2025, 7, 15), 17, 20, "Manager"), + Shift("S09", "Ana", date(2025, 7, 16), 9, 17, "Chef"), Shift("S10", "Ana", date(2025, 7, 18), 10, 16, "Cashier"), + Shift("S11", "Charlie", date(2025, 7, 14), 9, 17, "Cashier") +] + +print(f"\nInitial Score: {session.get_score()} (Hard|Medium|Soft)") + +print("\n--- Inserting all facts into the session ---") +session.insert_batch([employee_ana, employee_ben, policy] + shifts) +session.flush() + +print(f"\nScore after insert: {session.get_score()} (Hard|Medium|Soft)") + +print("\nConstraint Violations:") +matches = session.get_constraint_matches() +for constraint_id, violations in matches.items(): + # Use get_sum_abs() to show the total magnitude of the penalty. + total_penalty = sum(s.get_sum_abs() for s, _ in violations) + print(f" - {constraint_id} (Total Penalty: {total_penalty})") + for score, facts_tuple in violations: + facts_list = [f for f in (getattr(facts_tuple, attr, None) for attr in ['fact_a', 'fact_b', 'fact_c', 'fact_d', 'fact_e']) if f is not None] + print(f" - Violation with facts: {facts_list}, Score Impact: {score}") + +session.update_constraint_weight("Scheduled on unavailable day", 10.0) +session.update_constraint_weight("Overlapping shifts", 20.0) + +print(f"\nScore after updating constraint weights: {session.get_score()} (Hard|Medium|Soft)") + +print("\nConstraint Violations after updating constraint weights:") +matches = session.get_constraint_matches() +for constraint_id, violations in matches.items(): + # Use get_sum_abs() to show the total magnitude of the penalty. + total_penalty = sum(s.get_sum_abs() for s, _ in violations) + print(f" - {constraint_id} (Total Penalty: {total_penalty})") + for score, facts_tuple in violations: + facts_list = [f for f in (getattr(facts_tuple, attr, None) for attr in ['fact_a', 'fact_b', 'fact_c', 'fact_d', 'fact_e']) if f is not None] + print(f" - Violation with facts: {facts_list}, Score Impact: {score}") + +print("\n--- Retracting Ana's overlapping shift (S08) ---") +session.retract(next(s for s in shifts if s.shift_id == "S08")) +session.flush() +print(f"Score after retracting S08: {session.get_score()} (Hard|Medium|Soft)") + +print("\n--- Correcting Ana's skill-mismatch shift (S09) ---") +session.retract(next(s for s in shifts if s.shift_id == "S09")) +session.insert(Shift("S09-FIXED", "Ana", date(2025, 7, 16), 9, 17, "Manager")) +session.flush() +print(f"Score after correcting S09: {session.get_score()} (Hard|Medium|Soft)") + +print("\n--- Final Violations ---") +matches = session.get_constraint_matches() +for constraint_id, violations in matches.items(): + print(f" - {constraint_id}") + for score, facts_tuple in violations: + facts_list = [f for f in (getattr(facts_tuple, attr, None) for attr in ['fact_a', 'fact_b', 'fact_c', 'fact_d', 'fact_e']) if f is not None] + print(f" - Remaining violation with facts: {facts_list}") + +print("\n--- Example Complete ---") diff --git a/examples/greynet/greynet_example_node_sharing.py b/examples/greynet/greynet_example_node_sharing.py new file mode 100644 index 0000000..d26cefb --- /dev/null +++ b/examples/greynet/greynet_example_node_sharing.py @@ -0,0 +1,132 @@ +import time +import random +from dataclasses import dataclass +import tracemalloc + +# Assume all provided GreyJack files are in the classpath +from greyjack.score_calculation.greynet.greynet_fact import * +from greyjack.score_calculation.greynet.builder import ConstraintBuilder +from greyjack.score_calculation.scores.SimpleScore import SimpleScore + +# --- 1. Setup & Data Models --- + +@greynet_fact +@dataclass +class Transaction: + transaction_id: str + amount: float + country_code: str + +def format_bytes(num_bytes: int) -> str: + """Formats a byte count into a human-readable string (KB, MB, etc.).""" + if num_bytes < 1024: + return f"{num_bytes} B" + for unit in ['B', 'KB', 'MB', 'GB', 'TB']: + if num_bytes < 1024.0: + return f"{num_bytes:.1f} {unit}" + num_bytes /= 1024.0 + return f"{num_bytes:.1f} PB" + +# --- SOLUTION: Callable class to ensure unique node identity --- +class AmountThresholdFilter: + def __init__(self, threshold: float): + self.threshold = threshold + + def __call__(self, t: Transaction) -> bool: + return t.amount > self.threshold + + # The __eq__ and __hash__ methods are critical for the engine + # to correctly distinguish between different filter instances. + def __eq__(self, other): + return isinstance(other, AmountThresholdFilter) and self.threshold == other.threshold + + def __hash__(self): + return hash((self.__class__, self.threshold)) + +# --- 2. Rule Generation Logic --- + +def create_rules(builder: ConstraintBuilder, num_rules: int, with_sharing: bool): + """ + Programmatically generates and adds rules to a ConstraintBuilder. + """ + print(f"Generating {num_rules} rules {'with' if with_sharing else 'without'} node sharing...") + + for i in range(num_rules): + constraint_id = f"high_value_tx_{i}" + + if with_sharing: + # All rules share the exact same filter condition. + @builder.constraint(constraint_id, default_weight=1.0) + def shared_rule(): + return (builder.for_each(Transaction) + .filter(lambda t: t.amount > 5000) + .penalize_simple(lambda t: t.amount)) + else: + # Each rule now gets a unique filter object with its own state. + # This forces the engine to create a new AlphaNode for each rule. + def create_unique_rule(rule_index): + # Create a unique, stateful filter instance for each rule + unique_filter = AmountThresholdFilter(5000 + rule_index) + + @builder.constraint(f"high_value_tx_unique_{rule_index}", default_weight=1.0) + def unique_rule(): + return (builder.for_each(Transaction) + .filter(unique_filter) # Use the unique filter object + .penalize_simple(lambda t: t.amount)) + return unique_rule + + create_unique_rule(i) + +# --- 3. Benchmarking Harness (Unchanged) --- +def run_benchmark(num_rules: int, num_facts: int, with_sharing: bool): + scenario_name = "With Node Sharing" if with_sharing else "Without Node Sharing" + print(f"\n----- Running Benchmark: {scenario_name} -----") + + builder = ConstraintBuilder(name=f"scenario_{'shared' if with_sharing else 'unique'}", score_class=SimpleScore) + + start_build_time = time.perf_counter() + create_rules(builder, num_rules, with_sharing) + session = builder.build() + end_build_time = time.perf_counter() + build_duration = end_build_time - start_build_time + + tracemalloc.start() + facts = [ + Transaction(transaction_id=str(i), amount=random.uniform(1, 10000), country_code="US") + for i in range(num_facts) + ] + session = builder.build() + current, peak = tracemalloc.get_traced_memory() + tracemalloc.stop() + + start_insert_time = time.perf_counter() + session.insert_batch(facts) + session.flush() + end_insert_time = time.perf_counter() + insert_duration = end_insert_time - start_insert_time + + score = session.get_score() + print(f"Score: {score.simple_value}") + + return { + "Scenario": scenario_name, + "Build Time (s)": f"{build_duration:.4f}", + "Insertion Time (s)": f"{insert_duration:.4f}", + "Peak Memory": format_bytes(peak) + } + +if __name__ == '__main__': + NUM_RULES = 100 + NUM_FACTS = 50000 + + results_no_sharing = run_benchmark(NUM_RULES, NUM_FACTS, with_sharing=False) + results_with_sharing = run_benchmark(NUM_RULES, NUM_FACTS, with_sharing=True) + + print("\n\n--- BENCHMARK RESULTS ---") + print(f"Configuration: {NUM_RULES} rules, {NUM_FACTS} facts.") + print("---------------------------------------------------------------------") + print(f"| {'Scenario':<25} | {'Build Time (s)':<15} | {'Insertion Time (s)':<20} | {'Peak Memory':<15} |") + print("---------------------------------------------------------------------") + print(f"| {results_no_sharing['Scenario']:<25} | {results_no_sharing['Build Time (s)']:<15} | {results_no_sharing['Insertion Time (s)']:<20} | {results_no_sharing['Peak Memory']:<15} |") + print(f"| {results_with_sharing['Scenario']:<25} | {results_with_sharing['Build Time (s)']:<15} | {results_with_sharing['Insertion Time (s)']:<20} | {results_with_sharing['Peak Memory']:<15} |") + print("---------------------------------------------------------------------") diff --git a/examples/greynet/greynet_example_stress_1.py b/examples/greynet/greynet_example_stress_1.py new file mode 100644 index 0000000..66bb93d --- /dev/null +++ b/examples/greynet/greynet_example_stress_1.py @@ -0,0 +1,211 @@ +import time +import random +import tracemalloc +from dataclasses import dataclass +from typing import List, Dict, Set + +# --- Imports using the full, explicit path as requested --- +from greyjack.score_calculation.greynet.greynet_fact import * +from greyjack.score_calculation.greynet.builder import ConstraintBuilder, Collectors, JoinerType +from greyjack.score_calculation.scores.SimpleScore import SimpleScore + +# --- Data Definitions --- + +@greynet_fact +@dataclass() +class Customer: + id: int + risk_level: str # 'low', 'medium', 'high' + status: str # 'active', 'inactive' + +@greynet_fact +@dataclass() +class Transaction: + id: int + customer_id: int + amount: float + location: str + +@greynet_fact +@dataclass() +class SecurityAlert: + location: str + severity: int # 1 to 5 + +# --- Constraint Definitions --- + +def define_constraints(builder: ConstraintBuilder): + """ + Defines a set of rules to stress test various engine capabilities. + """ + + # Constraint 1: Simple filter for high-value transactions. + @builder.constraint("high_value_transaction") + def high_value_transaction(): + return builder.for_each(Transaction)\ + .filter(lambda tx: tx.amount > 45000)\ + .penalize_simple(lambda tx: tx.amount / 1000) + + # Constraint 2: Group transactions by customer and check for excessive activity. + @builder.constraint("excessive_transactions_per_customer") + def excessive_transactions(): + return builder.for_each(Transaction)\ + .group_by(lambda tx: tx.customer_id, Collectors.count())\ + .filter(lambda cid, count: count > 25)\ + .penalize_simple(lambda cid, count: (count - 25) * 10) + + # Constraint 3: Join transactions with security alerts on location. + @builder.constraint("transaction_in_alerted_location") + def suspicious_location_tx(): + return builder.for_each(Transaction)\ + .join(builder.for_each(SecurityAlert), + JoinerType.EQUAL, + lambda tx: tx.location, + lambda alert: alert.location)\ + .penalize_simple(lambda tx, alert: 100 * alert.severity) + + # Constraint 4: Join to find transactions from inactive customers. + @builder.constraint("inactive_customer_transaction") + def inactive_customer_tx(): + return builder.for_each(Customer)\ + .filter(lambda c: c.status == 'inactive')\ + .join(builder.for_each(Transaction), + JoinerType.EQUAL, + lambda c: c.id, + lambda tx: tx.customer_id)\ + .penalize_simple(500) + + # Constraint 5: Complex rule using if_not_exists. + # Penalize if a high-risk customer has a transaction in a location + # that does NOT have a security alert. + @builder.constraint("high_risk_transaction_without_alert") + def high_risk_no_alert(): + return builder.for_each(Customer)\ + .filter(lambda c: c.risk_level == 'high')\ + .join(builder.for_each(Transaction), + JoinerType.EQUAL, + lambda c: c.id, + lambda tx: tx.customer_id)\ + .if_not_exists( + SecurityAlert, + left_key=lambda c, tx: tx.location, + right_key=lambda alert: alert.location + )\ + .penalize_simple(1000) + + +# --- Data Generation --- + +def generate_data(num_customers: int, num_transactions: int, num_locations: int) -> Dict[str, list]: + """Generates a large, randomized dataset for testing.""" + print("Generating test data...") + locations = [f"location_{i}" for i in range(num_locations)] + + customers = [ + Customer( + id=i, + risk_level=random.choice(['low', 'medium', 'high']), + status=random.choices(['active', 'inactive'], weights=[0.95, 0.05], k=1)[0] + ) for i in range(num_customers) + ] + + transactions = [ + Transaction( + id=i, + customer_id=random.randint(0, num_customers - 1), + amount=random.uniform(1.0, 50000.0), + location=random.choice(locations) + ) for i in range(num_transactions) + ] + + # Create alerts for a subset of locations + alerted_locations = random.sample(locations, k=max(1, num_locations // 4)) + alerts = [ + SecurityAlert( + location=loc, + severity=random.randint(1, 5) + ) for loc in alerted_locations + ] + + return {"customers": customers, "transactions": transactions, "alerts": alerts} + +# --- Main Test Runner --- + +def main(): + """Main function to run the stress test and report results.""" + + # --- Configuration --- + NUM_CUSTOMERS = 10_000 + NUM_TRANSACTIONS = 100_000 + NUM_LOCATIONS = 1_000 + + print("### Starting Rule Engine Stress Test (Windows Compatible) ###") + + # 1. Setup Phase & Initial State + tracemalloc.start() + + time_start_setup = time.perf_counter() + builder = ConstraintBuilder(name="stress-test-session", score_class=SimpleScore) + define_constraints(builder) + session = builder.build() + time_end_setup = time.perf_counter() + + # 2. Data Generation Phase + time_start_data = time.perf_counter() + data = generate_data(NUM_CUSTOMERS, NUM_TRANSACTIONS, NUM_LOCATIONS) + all_facts = data["customers"] + data["transactions"] + data["alerts"] + time_end_data = time.perf_counter() + + # 3. Processing Phase + print("Inserting facts and processing rules...") + time_start_processing = time.perf_counter() + session.insert_batch(all_facts) + final_score = session.get_score() + matches = session.get_constraint_matches() + time_end_processing = time.perf_counter() + + # 4. Get Memory Snapshot + mem_current, mem_peak = tracemalloc.get_traced_memory() + tracemalloc.stop() + + # 5. Reporting + print("\n--- Stress Test Results ---") + + # Time Metrics + setup_duration = time_end_setup - time_start_setup + data_gen_duration = time_end_data - time_start_data + processing_duration = time_end_processing - time_start_processing + total_duration = time_end_processing - time_start_setup + + # Performance Metrics + total_facts = len(all_facts) + facts_per_second = total_facts / processing_duration if processing_duration > 0 else float('inf') + + # Display Report using Markdown + print("\n#### Performance Summary") + print(f"| Metric | Value |") + print(f"|--------------------------------|---------------------|") + print(f"| Total Facts Processed | {total_facts:,} |") + print(f"| Setup Time (Build Network) | {setup_duration:.4f} s |") + print(f"| Data Generation Time | {data_gen_duration:.4f} s |") + print(f"| **Processing Time (Insert+Flush)** | **{processing_duration:.4f} s** |") + print(f"| Total Time | {total_duration:.4f} s |") + print(f"| **Throughput** | **{facts_per_second:,.2f} facts/sec** |") + + print("\n#### Memory Usage Summary (via tracemalloc)") + print(f"| Metric | Value |") + print(f"|--------------------------------|---------------------|") + print(f"| Final Memory Usage | {mem_current / 1024**2:.2f} MB |") + print(f"| **Peak Memory Usage** | **{mem_peak / 1024**2:.2f} MB** |") + + + print("\n#### Engine Output") + print(f"- **Final Score:** {final_score}") + print(f"- **Total Constraint Matches:** {sum(len(v) for v in matches.values())}") + for constraint_id, match_list in sorted(matches.items()): + print(f" - `{constraint_id}`: {len(match_list)} matches") + +if __name__ == "__main__": + # To run this script, place it in a location where it can import the + # 'greyjack' package correctly, as per your project structure. + main() diff --git a/examples/greynet/greynet_example_stress_2.py b/examples/greynet/greynet_example_stress_2.py new file mode 100644 index 0000000..7c2c5a7 --- /dev/null +++ b/examples/greynet/greynet_example_stress_2.py @@ -0,0 +1,364 @@ +# ============================================================================== +# Advanced Stress Test for the GreyJack Rule Engine +# Scenario: Algorithmic Trading Compliance Monitoring +# ============================================================================== + +import time +import random +import tracemalloc +from dataclasses import dataclass, field +from datetime import datetime, timedelta, timezone +from typing import List, Dict, Set, Tuple, Any +from collections import defaultdict + +# --- Imports from the GreyJack Rule Engine --- +# Assuming 'greyjack' package is available in the python path +from greyjack.score_calculation.greynet.greynet_fact import * +from greyjack.score_calculation.greynet.builder import ConstraintBuilder, Collectors +from greyjack.score_calculation.greynet.common.joiner_type import JoinerType +# Assuming this import is correct as per your project structure +from greyjack.score_calculation.scores.HardSoftScore import HardSoftScore + +# ============================================================================== +# 2. Data Model Definitions +# ============================================================================== + +@greynet_fact +@dataclass() +class Trader: + id: int + name: str + desk: str + risk_limit: float + is_insider: bool = False + +@greynet_fact +@dataclass() +class Security: + ticker: str + sector: str + is_restricted: bool = False + +@greynet_fact +@dataclass() +class Trade: + id: int + trader_id: int + ticker: str + quantity: int + price: float + timestamp: datetime + side: str # 'BUY' or 'SELL' + +@greynet_fact +@dataclass() +class MarketNews: + id: int + related_tickers: Tuple[str, ...] + headline: str + timestamp: datetime + is_material: bool = True + +# ============================================================================== +# 3. Helper Functions for Complex Rule Logic +# ============================================================================== + +def has_painting_the_tape_pattern(trades: List[Trade]) -> bool: + """ + Checks a list of trades for a specific manipulative pattern. + Pattern: Two small buys followed by a large sell in a short time frame. + """ + if len(trades) < 3: + return False + sorted_trades = sorted(trades, key=lambda t: t.timestamp) + + for i in range(len(sorted_trades) - 2): + window = sorted_trades[i:i+3] + if (window[2].timestamp - window[0].timestamp) > timedelta(minutes=2): + continue + + is_pattern = ( + window[0].side == 'BUY' and window[0].quantity < 100 and + window[1].side == 'BUY' and window[1].quantity < 100 and + window[2].side == 'SELL' and window[2].quantity > 500 + ) + if is_pattern: + return True + return False + +def count_trades_in_windows(trades: List[Trade]) -> int: + """ + Counts violations within a list of trades. + A violation is > 2 trades in any 5-minute tumbling window. + """ + window_size_sec = timedelta(minutes=5).total_seconds() + total_violations = 0 + + window_counts = defaultdict(int) + epoch = datetime(1970, 1, 1, tzinfo=timezone.utc).timestamp() + + for trade in trades: + window_index = int((trade.timestamp.timestamp() - epoch) // window_size_sec) + window_counts[window_index] += 1 + + for count in window_counts.values(): + if count > 2: + total_violations += (count - 2) + + return total_violations + +# ============================================================================== +# 4. Intricate and Complex Constraint Definitions +# ============================================================================== + +def define_advanced_constraints(builder: ConstraintBuilder): + """ + Defines complex compliance rules for algorithmic trading. + """ + + # --- Rule 1: Market Manipulation ("Painting the Tape") --- + @builder.constraint("market_manipulation_painting_the_tape") + def painting_the_tape(): + return builder.for_each(Trade) \ + .group_by( + lambda trade: (trade.trader_id, trade.ticker), + Collectors.to_list() + ) \ + .filter( + lambda key, trades: has_painting_the_tape_pattern(trades) + ) \ + .penalize_hard(10000) + + + # --- Rule 2: Exceeding Daily Risk Limits --- + @builder.constraint("trader_exceeds_daily_risk_limit") + def trader_risk_limit(): + daily_trade_volumes = builder.for_each(Trade) \ + .group_by( + lambda trade: (trade.trader_id, trade.timestamp.date()), + Collectors.compose({ + 'total_volume': Collectors.sum(lambda t: t.price * t.quantity), + 'trade_count': Collectors.count() + }) + ) + + # FIXED: The join is reversed. We start from the aggregated stream + # and join the Trader facts to it. This uses a different node + # that preserves all necessary data. + return daily_trade_volumes \ + .join(builder.for_each(Trader), + JoinerType.EQUAL, + # Left key func: extracts trader_id from the group key + lambda key, result: key[0], + # Right key func: extracts trader_id from the trader object + lambda trader: trader.id + ) \ + .filter( + # The lambda now receives (key, result_dict, trader). + lambda key, result_dict, trader: result_dict['total_volume'] > trader.risk_limit + ) \ + .penalize_hard( + # The penalty lambda also receives all three facts. + lambda key, result_dict, trader: (result_dict['total_volume'] - trader.risk_limit) / 1000 + ) + + + # --- Rule 3: Potential Insider Trading --- + @builder.constraint("potential_insider_trading") + def insider_trading(): + insider_trades = builder.for_each(Trader)\ + .filter(lambda trader: trader.is_insider)\ + .join(builder.for_each(Trade), + JoinerType.EQUAL, + lambda trader: trader.id, + lambda trade: trade.trader_id + ) + + return insider_trades.join(builder.for_each(MarketNews), + JoinerType.EQUAL, + lambda trader, trade: trade.ticker, + lambda news: news.related_tickers[0] if news.related_tickers else None + ).filter(lambda trader, trade, news: + news.is_material and + timedelta(minutes=1) < (news.timestamp - trade.timestamp) < timedelta(hours=1) + ).penalize_hard(50000) + + + # --- Rule 4: Restricted Security Trading Bursts --- + @builder.constraint("restricted_security_trading_burst") + def restricted_trading_burst(): + restricted_trades = builder.for_each(Trade) \ + .if_exists( + builder.for_each(Security).filter(lambda s: s.is_restricted), + lambda trade: trade.ticker, + lambda sec: sec.ticker + ) + + return restricted_trades.group_by( + lambda trade: trade.ticker, + Collectors.to_list() + ).filter( + lambda ticker, trades: count_trades_in_windows(trades) > 0 + ).penalize_soft( + lambda ticker, trades: count_trades_in_windows(trades) * 50 + ) + +# ============================================================================== +# 5. Data Generation +# ============================================================================== + +def generate_advanced_data(num_traders, num_securities, num_trades) -> Dict[str, list]: + """Generates a large, randomized dataset with specific patterns for detection.""" + print("Generating advanced test data with specific patterns...") + start_time = datetime.now(timezone.utc) - timedelta(days=1) + + # --- Generate Base Entities --- + traders = [ + Trader( + id=i, name=f"Trader_{i}", desk=random.choice(['Equities', 'FX', 'Derivatives']), + risk_limit=random.uniform(5_000_000, 20_000_000), + is_insider= (i % 20 == 0) # 5% of traders are insiders + ) for i in range(num_traders) + ] + securities = [ + Security( + ticker=f"SEC_{i}", sector=random.choice(['Tech', 'Health', 'Finance']), + is_restricted=(i % 15 == 0) # ~6.7% of securities are restricted + ) for i in range(num_securities) + ] + + facts: Dict[str, list] = {"traders": traders, "securities": securities, "trades": [], "news": []} + trade_id_counter = 0 + + # --- Inject Pattern: Painting the Tape --- + victim_security = securities[1] + manipulator_trader = traders[1] + print(f" Injecting 'Painting the Tape' pattern for {manipulator_trader.name} on {victim_security.ticker}...") + base_ts = start_time + timedelta(hours=1) + facts["trades"].append(Trade(trade_id_counter, manipulator_trader.id, victim_security.ticker, 50, 100.0, base_ts, 'BUY')); trade_id_counter+=1 + facts["trades"].append(Trade(trade_id_counter, manipulator_trader.id, victim_security.ticker, 60, 100.5, base_ts + timedelta(seconds=30), 'BUY')); trade_id_counter+=1 + facts["trades"].append(Trade(trade_id_counter, manipulator_trader.id, victim_security.ticker, 600, 101.0, base_ts + timedelta(seconds=90), 'SELL')); trade_id_counter+=1 + + # --- Inject Pattern: Risk Limit Breach --- + risky_trader = traders[2] + # Set a specific, predictable risk limit to breach + risky_trader = Trader(id=risky_trader.id, name=risky_trader.name, desk=risky_trader.desk, risk_limit=24_000_000, is_insider=risky_trader.is_insider) + facts["traders"][2] = risky_trader # Replace the old trader fact + print(f" Injecting 'Risk Limit Breach' pattern for {risky_trader.name} (Limit: {risky_trader.risk_limit})...") + # 5 trades of 5M each = 25M total volume, which is > 24M limit + for i in range(5): + facts["trades"].append(Trade(trade_id_counter, risky_trader.id, securities[10].ticker, 10000, 500.0, start_time + timedelta(hours=2, minutes=i), 'BUY')); trade_id_counter+=1 + + # --- Inject Pattern: Insider Trading --- + insider_trader = next(t for t in traders if t.is_insider) + insider_security = securities[5] + print(f" Injecting 'Insider Trading' pattern for {insider_trader.name} on {insider_security.ticker}...") + trade_ts = start_time + timedelta(hours=3) + news_ts = trade_ts + timedelta(minutes=30) + facts["trades"].append(Trade(trade_id_counter, insider_trader.id, insider_security.ticker, 5000, 250.0, trade_ts, 'BUY')); trade_id_counter+=1 + facts["news"].append(MarketNews(0, (insider_security.ticker,), "BREAKING: SEC_5 to be acquired by major rival!", news_ts, True)) + + # --- Inject Pattern: Restricted Trading Burst --- + restricted_sec = next(s for s in securities if s.is_restricted) + burst_trader = traders[3] + print(f" Injecting 'Restricted Trading Burst' on {restricted_sec.ticker}...") + burst_ts = start_time + timedelta(hours=4) + # 4 trades in the same 5-min window will trigger a penalty of (4-2)=2 * 50 + for i in range(4): + facts["trades"].append(Trade(trade_id_counter, burst_trader.id, restricted_sec.ticker, 100, 50.0, burst_ts + timedelta(minutes=i), 'SELL')); trade_id_counter+=1 + + # --- Generate Random "Noise" Trades --- + print(f" Generating {num_trades} random noise trades...") + for _ in range(num_trades): + trader = random.choice(traders) + security = random.choice(securities) + ts = start_time + timedelta(seconds=random.randint(0, 86400)) + facts["trades"].append(Trade( + id=trade_id_counter, trader_id=trader.id, ticker=security.ticker, + quantity=random.randint(10, 1000), price=random.uniform(10, 1000), + timestamp=ts, side=random.choice(['BUY', 'SELL']) + )); trade_id_counter+=1 + + return facts + +# ============================================================================== +# 6. Main Test Runner +# ============================================================================== + +def main(): + """Main function to run the stress test and report results.""" + + # --- Configuration --- + NUM_TRADERS = 100 + NUM_SECURITIES = 2000 + NUM_TRADES = 100_000 + + print("### Starting Advanced Rule Engine Stress Test ###") + + # 1. Setup Phase & Initial State + tracemalloc.start() + + time_start_setup = time.perf_counter() + builder = ConstraintBuilder(name="adv-stress-test", score_class=HardSoftScore) + define_advanced_constraints(builder) + session = builder.build() + time_end_setup = time.perf_counter() + + # 2. Data Generation Phase + time_start_data = time.perf_counter() + data = generate_advanced_data(NUM_TRADERS, NUM_SECURITIES, NUM_TRADES) + all_facts = data["traders"] + data["securities"] + data["trades"] + data["news"] + time_end_data = time.perf_counter() + + # 3. Processing Phase + print("\nInserting facts and processing rules...") + time_start_processing = time.perf_counter() + session.insert_batch(all_facts) + final_score = session.get_score() + matches = session.get_constraint_matches() + time_end_processing = time.perf_counter() + + # 4. Get Memory Snapshot + mem_current, mem_peak = tracemalloc.get_traced_memory() + tracemalloc.stop() + + # 5. Reporting + print("\n--- Stress Test Results ---") + + # Time Metrics + setup_duration = time_end_setup - time_start_setup + data_gen_duration = time_end_data - time_start_data + processing_duration = time_end_processing - time_start_processing + total_duration = time_end_processing - time_start_setup + + # Performance Metrics + total_facts = len(all_facts) + facts_per_second = total_facts / processing_duration if processing_duration > 0 else float('inf') + + # Display Report using Markdown Table + print("\n#### Performance Summary") + print(f"| Metric | Value |") + print(f"|--------------------------------|---------------------|") + print(f"| Total Facts Processed | {total_facts:,} |") + print(f"| Setup Time (Build Network) | {setup_duration:.4f} s |") + print(f"| Data Generation Time | {data_gen_duration:.4f} s |") + print(f"| **Processing Time (Insert+Flush)** | **{processing_duration:.4f} s** |") + print(f"| Total Time | {total_duration:.4f} s |") + print(f"| **Throughput** | **{facts_per_second:,.2f} facts/sec** |") + + print("\n#### Memory Usage Summary (via tracemalloc)") + print(f"| Metric | Value |") + print(f"|--------------------------------|---------------------|") + print(f"| Final Memory Usage | {mem_current / 1024**2:.2f} MB |") + print(f"| **Peak Memory Usage** | **{mem_peak / 1024**2:.2f} MB** |") + + + print("\n#### Engine Output") + print(f"- **Final Score:** {final_score}") + print(f"- **Total Constraint Matches:** {sum(len(v) for v in matches.values())}") + for constraint_id, match_list in sorted(matches.items()): + print(f" - `{constraint_id}`: {len(match_list)} matches found.") + +if __name__ == "__main__": + main() + diff --git a/examples/greynet/greynet_example_subqueries.py b/examples/greynet/greynet_example_subqueries.py new file mode 100644 index 0000000..18efea3 --- /dev/null +++ b/examples/greynet/greynet_example_subqueries.py @@ -0,0 +1,212 @@ +# main_complex_example.py +from dataclasses import dataclass, field +from typing import Set + +from greyjack.score_calculation.greynet.greynet_fact import * +from greyjack.score_calculation.greynet.builder import ConstraintBuilder, JoinerType +from greyjack.score_calculation.scores.HardMediumSoftScore import HardMediumSoftScore + +# --- Data Models --- + +@greynet_fact +@dataclass() +class Client: + client_id: str + name: str + status: str # e.g., 'VIP', 'Standard' + +@greynet_fact +@dataclass() +class Project: + project_id: str + client_id: str + primary_skill: str # Simplified to one primary skill for clarity + budget: float + +@greynet_fact +@dataclass() +class Employee: + employee_id: str + name: str + skills: Set[str] + +@greynet_fact +@dataclass() +class Assignment: + assignment_id: str + project_id: str + employee_id: str + status: str # e.g., 'Active', 'Completed' + +@greynet_fact +@dataclass() +class Invoice: + invoice_id: str + project_id: str + amount: float + +@greynet_fact +@dataclass() +class TrainingModule: + module_id: str + skill_taught: str + +@greynet_fact +@dataclass() +class TrainingCompletion: + employee_id: str + module_id: str + +# --- Constraint Definitions --- +builder = ConstraintBuilder(name="talent-management", score_class=HardMediumSoftScore) + +# --- +# Rule 1: Missing Invoice for VIP Client Project +# --- +@builder.constraint("Missing Invoice for VIP Project") +def missing_invoice_for_vip_project(): + active_assignments = builder.for_each(Assignment).filter(lambda a: a.status == 'Active') + + vip_projects = ( + active_assignments.join( + builder.for_each(Project), + JoinerType.EQUAL, + left_key_func=lambda a: a.project_id, + right_key_func=lambda p: p.project_id + ).join( + builder.for_each(Client), + JoinerType.EQUAL, + left_key_func=lambda a, p: p.client_id, + right_key_func=lambda c: c.client_id + ).filter(lambda a, p, c: c.status == 'VIP') + ) + + return ( + vip_projects.if_not_exists( + Invoice, + left_key=lambda a, p, c: p.project_id, + right_key=lambda inv: inv.project_id + ).penalize_medium(250) + ) + +# --- +# Rule 2: Training Dead-End Assignment +# --- +@builder.constraint("Training Dead-End Assignment") +def training_dead_end(): + assignments = builder.for_each(Assignment).join( + builder.for_each(Employee), + JoinerType.EQUAL, + lambda a: a.employee_id, + lambda e: e.employee_id + ).join( + builder.for_each(Project), + JoinerType.EQUAL, + lambda a, e: a.project_id, + lambda p: p.project_id + ) + + # Logic Fix: Added filter for 'Active' status to avoid flagging completed projects. + mismatched_assignments = assignments.filter( + lambda a, e, p: a.status == 'Active' and p.primary_skill not in e.skills + ) + + return ( + mismatched_assignments.if_not_exists( + TrainingModule, + left_key=lambda a, e, p: p.primary_skill, + right_key=lambda tm: tm.skill_taught + ).penalize_hard(1000) + ) + +# --- +# Rule 3: Bonus for Trained Employee on VIP Project Completion +# --- +@builder.constraint("Bonus Flag: VIP Project Completion by Trained Employee") +def bonus_for_vip_completion(): + completed_vip_assignments = ( + builder.for_each(Assignment) + .filter(lambda a: a.status == 'Completed') + .join(builder.for_each(Project), JoinerType.EQUAL, lambda a: a.project_id, lambda p: p.project_id) + .join(builder.for_each(Client), JoinerType.EQUAL, lambda a, p: p.client_id, lambda c: c.client_id) + .filter(lambda a, p, c: c.status == 'VIP') + ) + + return ( + completed_vip_assignments.if_exists( + TrainingCompletion, + left_key=lambda a, p, c: (a.employee_id, 'CR-101'), + right_key=lambda tc: (tc.employee_id, tc.module_id) + ).penalize_soft(1) + ) + +# --- Execution and Verification --- +print("--- Building the session ---") +session = builder.build() + +# --- Initial Data Setup --- +client_vip = Client("C01", "GlobalTech Inc.", "VIP") +client_std = Client("C02", "Local Motors", "Standard") +emp_ana = Employee("E01", "Ana", skills={"Python", "SQL"}) +emp_ben = Employee("E02", "Ben", skills={"Java"}) +emp_carl = Employee("E03", "Carl", skills={"SQL"}) +proj_ai = Project("P01", "C01", "AI/ML", 100_000) +proj_java = Project("P02", "C02", "Java", 50_000) +proj_cloud = Project("P03", "C01", "CloudInfra", 75_000) +train_python = TrainingModule("TM-01", "Python") +train_relations = TrainingModule("CR-101", "Customer Relations") +ana_training_completion = TrainingCompletion("E01", "CR-101") +assign_ana_ai = Assignment("A01", "P01", "E01", "Active") +assign_ben_java = Assignment("A02", "P02", "E02", "Active") +assign_carl_cloud = Assignment("A03", "P03", "E03", "Active") + +initial_facts = [ + client_vip, client_std, + emp_ana, emp_ben, emp_carl, + proj_ai, proj_java, proj_cloud, + train_python, train_relations, ana_training_completion, + assign_ana_ai, assign_ben_java, assign_carl_cloud +] + +def print_violations(): + matches = session.get_constraint_matches() + print(f"\nScore: {session.get_score()} (Hard|Medium|Soft)") + if not matches: + print(" No violations found.") + return + for constraint_id, violations in matches.items(): + print(f" - Violation: '{constraint_id}'") + for score, facts in violations: + # Display only the first fact for brevity in complex joins + fact_display = getattr(facts, 'fact_a', facts) + print(f" - Details: {fact_display}, Score Impact: {score}") + +# --- 1. Initial State --- +print("\n--- 1. Evaluating Initial State ---") +session.insert_batch(initial_facts) +print_violations() + +# --- 2. Resolving Violations --- +print("\n\n--- 2. Resolving Violations by Adding Facts ---") +invoice_for_ai = Invoice("INV-001", "P01", 100_000) +print(f"\nAction: Inserting invoice for project {invoice_for_ai.project_id}...") +session.insert(invoice_for_ai) +print_violations() + +train_cloud = TrainingModule("TM-03", "CloudInfra") +print(f"\nAction: Adding training module for skill '{train_cloud.skill_taught}'...") +session.insert(train_cloud) +print_violations() + +# --- 3. Triggering a New 'Bonus' Flag --- +print("\n\n--- 3. Triggering a Bonus by Completing a Project ---") +completed_assign_ana_ai = Assignment("A01", "P01", "E01", "Completed") +print("\nAction: Ana completes the VIP AI project...") +session.retract(assign_ana_ai) +session.insert(completed_assign_ana_ai) +print_violations() + +# --- 4. Final State --- +print("\n\n--- 4. Final State ---") +print("The system has dynamically adapted to changes, resolving hard/medium violations") +print("and flagging a new soft-priority item (the bonus).") diff --git a/examples/greynet/greynet_example_temporal_1.py b/examples/greynet/greynet_example_temporal_1.py new file mode 100644 index 0000000..b961d6a --- /dev/null +++ b/examples/greynet/greynet_example_temporal_1.py @@ -0,0 +1,225 @@ +import uuid +from dataclasses import dataclass +from datetime import datetime, timedelta, timezone +from collections import deque +from greyjack.score_calculation.scores.SimpleScore import SimpleScore +from greyjack.score_calculation.greynet.greynet_fact import * +from greyjack.score_calculation.greynet.builder import ConstraintBuilder, Collectors +from greyjack.score_calculation.greynet.common.joiner_type import JoinerType + + +# TODO: Fix temporal, sequential features +# TODO: Add debug, tracing for temporal, sequential features + + +@greynet_fact +@dataclass +class UserEvent: + user_id: str + timestamp: datetime + +@greynet_fact +@dataclass +class FundDeposit(UserEvent): + amount: float + currency: str + +@greynet_fact +@dataclass +class TradeOrder(UserEvent): + amount: float + asset_pair: str + order_type: str + +@greynet_fact +@dataclass +class FundWithdrawal(UserEvent): + amount: float + to_address: str + +@greynet_fact +@dataclass +class AccountFlag(UserEvent): + reason: str + expires_at: datetime + + +def find_deposit_withdrawal_sequences(user_id, events): + pattern_steps = [ + lambda fact: isinstance(fact, FundDeposit) and fact.amount >= 10000, + lambda fact: isinstance(fact, FundWithdrawal) and fact.amount >= 9000 + ] + within_delta = timedelta(hours=24) + sorted_events = sorted(events, key=lambda e: e.timestamp) + + for i in range(len(sorted_events)): + if pattern_steps[0](sorted_events[i]): + for j in range(i + 1, len(sorted_events)): + event1 = sorted_events[i] + event2 = sorted_events[j] + if event2.timestamp - event1.timestamp > within_delta: + break + if pattern_steps[1](event2): + yield [event1, event2] + +def find_high_velocity_windows_for_user(withdrawals: list): + """ + Applies a continuous sliding window to a single user's sorted withdrawals + to find periods of high activity. + """ + if not withdrawals: + return + + window_size = timedelta(hours=1) + threshold = 100000 + + sorted_w = sorted(withdrawals, key=lambda w: w.timestamp) + + window = deque() + current_sum = 0.0 + yielded_violations = set() + + for w in sorted_w: + window.append(w) + current_sum += w.amount + + while w.timestamp - window[0].timestamp > window_size: + removed_fact = window.popleft() + current_sum -= removed_fact.amount + + if current_sum > threshold: + violation_key = frozenset(window) + if violation_key not in yielded_violations: + yield list(window) + yielded_violations.add(violation_key) + + +class FraudDetection: + def __init__(self): + self.builder = ConstraintBuilder(name="FraudDetectionSystem", score_class=SimpleScore) + self.define_constraints() + + def define_constraints(self): + """Uses the constraint decorator to define all detection rules.""" + + # Rule 1: High-Frequency Trading + @self.builder.constraint("HIGH_FREQ_TRADING", default_weight=1.0) + def high_frequency_trading(): + five_min_bucket = lambda ts: int(ts.timestamp() // 300) + return (self.builder.for_each(TradeOrder) + .group_by( + group_key_function=lambda trade: (trade.user_id, five_min_bucket(trade.timestamp)), + collector_supplier=Collectors.to_list() + ) + .filter(lambda group_key, trades: len(trades) > 50) + .penalize_simple(lambda group_key, trades: (len(trades) - 50) * 10) + ) + + # Rule 2: Suspicious Deposit -> Withdrawal Sequence + @self.builder.constraint("SUSPICIOUS_SEQUENCE", default_weight=1.0) + def suspicious_deposit_withdrawal_sequence(): + return (self.builder.for_each(UserEvent) + .group_by( + group_key_function=lambda event: event.user_id, + collector_supplier=Collectors.to_list() + ) + .flat_map(lambda user_id, events: find_deposit_withdrawal_sequences(user_id, events)) + .penalize_simple(5000) + ) + + @self.builder.constraint("HIGH_VELOCITY_WITHDRAWALS", default_weight=1.0) + def high_velocity_withdrawals(): + return (self.builder.for_each(FundWithdrawal) + .group_by( + group_key_function=lambda w: w.user_id, + collector_supplier=Collectors.to_list() + ) + .flat_map(lambda user_id, withdrawals: find_high_velocity_windows_for_user(withdrawals)) + .penalize_simple(lambda facts: sum(w.amount for w in facts) - 100000) + ) + + # Rule 4: Trading While Under Account Flag (Unchanged) + @self.builder.constraint("TRADING_WHILE_FLAGGED", default_weight=1.0) + def trading_while_flagged(): + high_value_trades = self.builder.for_each(TradeOrder).filter(lambda t: t.amount > 20000) + account_flags = self.builder.for_each(AccountFlag) + return (high_value_trades + .join(account_flags, + JoinerType.EQUAL, + left_key_func=lambda trade: trade.user_id, + right_key_func=lambda flag: flag.user_id + ) + .filter(lambda trade, flag: flag.timestamp <= trade.timestamp <= flag.expires_at) + .penalize_simple(1500) + ) + + def get_session(self): + return self.builder.build() + + +def run_simulation(): + print("### Setting up Fraud Detection System with Corrected Logic ###") + fraud_system = FraudDetection() + session = fraud_system.get_session() + + user_A = "user-Alice-123" + user_B = "user-Bob-456" + user_C = "user-Charlie-789" + + start_time = datetime.now(timezone.utc) + events = [] + + print("\n -> Generating data for [TRADING_WHILE_FLAGGED]") + events.append(AccountFlag(user_id=user_A, timestamp=start_time, reason="KYC_REVIEW", expires_at=start_time + timedelta(days=7))) + events.append(TradeOrder(user_id=user_A, timestamp=start_time + timedelta(hours=1), amount=25000, asset_pair="BTC/USD", order_type="SELL")) + + print(" -> Generating data for [SUSPICIOUS_SEQUENCE]") + events.append(FundDeposit(user_id=user_B, timestamp=start_time + timedelta(minutes=10), amount=15000, currency="USDC")) + events.append(FundWithdrawal(user_id=user_B, timestamp=start_time + timedelta(hours=5), amount=14950, to_address="0xabc...def")) + + print(" -> Generating data for [HIGH_FREQ_TRADING]") + for i in range(55): + events.append(TradeOrder(user_id=user_C, timestamp=start_time + timedelta(seconds=i*2), amount=100, asset_pair="ETH/USD", order_type="BUY")) + + print(" -> Generating data for [HIGH_VELOCITY_WITHDRAWALS]") + events.append(FundWithdrawal(user_id=user_A, timestamp=start_time + timedelta(minutes=30), amount=60000, to_address="0x123...456")) + events.append(FundWithdrawal(user_id=user_B, timestamp=start_time + timedelta(minutes=45), amount=45000, to_address="0x789...012")) + + print("\n### Inserting Events into Session ###") + session.insert_batch(events) + + print("\n### Calculating Initial Score ###") + print("Expected Score: 50 (HFT) + 10000 (2x Sequence) + 0 (High Velocity) + 1500 (Flagged) = 11550") + initial_score = session.get_score() + print(f"Actual Initial Total Score: {initial_score.simple_value}") + + print("\n### Retrieving Constraint Matches ###") + matches = session.get_constraint_matches() + + print("\n```mermaid") + print("graph TD") + print(" subgraph Detected Violations") + if not matches: + print(" No Violations Detected") + for constraint_id, violations in matches.items(): + print(f" {constraint_id} -- has {len(violations)} violation(s) --> V_{constraint_id}") + for i, violation in enumerate(violations): + score_object, _ = violation + score_val = score_object.simple_value + fact_summary = f"Penalty: {score_val:.2f}" + print(f" V_{constraint_id} -- \"{fact_summary}\" --> V_{constraint_id}_{i}") + print(" end") + print("```") + + print("\n### Demonstrating Dynamic Weight Update ###") + print(" -> Increasing weight of 'TRADING_WHILE_FLAGGED' from 1.0 to 3.0") + session.update_constraint_weight("TRADING_WHILE_FLAGGED", 3.0) + + updated_score = session.get_score() + print(f"Expected Updated Score: 11550 - 1500 + (1500 * 3) = 14550") + print(f"Actual Updated Total Score: {updated_score.simple_value}") + print(f"Score increased by: {updated_score.simple_value - initial_score.simple_value:.2f}") + + +if __name__ == "__main__": + run_simulation() diff --git a/examples/greynet/greynet_example_temporal_2.py b/examples/greynet/greynet_example_temporal_2.py new file mode 100644 index 0000000..a51cc89 --- /dev/null +++ b/examples/greynet/greynet_example_temporal_2.py @@ -0,0 +1,177 @@ +from dataclasses import dataclass +from datetime import datetime, timedelta +from typing import Union + +# Assuming all provided files are in a structured 'greyjack' directory +from greyjack.score_calculation.greynet.greynet_fact import * +from greyjack.score_calculation.greynet.builder import ConstraintBuilder, Collectors +from greyjack.score_calculation.greynet.common.joiner_type import JoinerType +from greyjack.score_calculation.scores.HardSoftScore import HardSoftScore + +# TODO: Fix temporal, sequential features +# TODO: Add debug, tracing for temporal, sequential features + +# --- Data Models --- + +@greynet_fact +@dataclass() +class LoginAttempt: + user: str + ip_address: str + timestamp: datetime + successful: bool + +@greynet_fact +@dataclass() +class FileAccess: + user: str + file_path: str + operation: str # 'read', 'write', 'delete' + timestamp: datetime + +@greynet_fact +@dataclass() +class NetworkConnection: + user: str + dest_ip: str + dest_port: int + timestamp: datetime + +@greynet_fact +@dataclass() +class AdminUser: + username: str + +# A Union type for convenience when starting the stream +SystemEvent = Union[LoginAttempt, FileAccess, NetworkConnection] + +def define_security_constraints(builder: ConstraintBuilder): + """ + Defines the sequence detection constraint. + + Pattern: + 1. A failed login attempt. + 2. Within 5 mins, a successful login for the SAME user from a DIFFERENT IP. + 3. Within 10 mins of success, the user accesses a sensitive file in /etc/. + 4. Within 2 mins of file access, the user makes an outbound connection on a known suspicious port (6667). + 5. The rule only triggers if the user is NOT a registered administrator. + """ + + # Helper predicate to validate the logic of the detected sequence + def validate_complex_sequence(sequence: list[SystemEvent]) -> bool: + # sequence[0] = Failed Login + # sequence[1] = Successful Login + # sequence[2] = File Access + # sequence[3] = Network Connection + + failed_login, successful_login, file_access, network_conn = sequence + + # Condition 1: Same user throughout the sequence + user = failed_login.user + if not (user == successful_login.user == file_access.user == network_conn.user): + return False + + # Condition 2: Successful login from a different IP + if failed_login.ip_address == successful_login.ip_address: + return False + + # Condition 3: Check intra-sequence time gaps + if not (successful_login.timestamp - failed_login.timestamp <= timedelta(minutes=5)): + return False + if not (file_access.timestamp - successful_login.timestamp <= timedelta(minutes=10)): + return False + if not (network_conn.timestamp - file_access.timestamp <= timedelta(minutes=2)): + return False + + return True + + @builder.constraint("anomalous_access_and_exfiltration", default_weight=1.0) + def detect_attack_pattern(): + + # The sequence of event types we are looking for + pattern_steps = [ + lambda e: isinstance(e, LoginAttempt) and not e.successful, + lambda e: isinstance(e, LoginAttempt) and e.successful, + lambda e: isinstance(e, FileAccess) and e.file_path.startswith('/etc/'), + lambda e: isinstance(e, NetworkConnection) and e.dest_port == 6667, + ] + + return ( + builder.for_each(SystemEvent) + # --- Start of Bug Fix --- + .sequence( + lambda e: e.timestamp, # time_extractor is the first argument + *pattern_steps, # Unpack steps as positional arguments + within=timedelta(minutes=15)# The entire sequence must complete within 15 mins + ) + # --- End of Bug Fix --- + # The stream now contains UniTuples where fact_a is a list of the 4 events + .filter(lambda seq: validate_complex_sequence(seq)) + + # Transform the stream of [event_list] into a stream of [username] + .flat_map(lambda seq: [seq[0].user]) + + # Now, check if this user exists in the AdminUser fact source. + # Only propagate the username if they DO NOT exist in the admin list. + .if_not_exists( + AdminUser, + left_key=lambda user_fact: user_fact, # The username from flat_map + right_key=lambda admin_fact: admin_fact.username + ) + # The stream now contains only usernames of non-admins who triggered the pattern. + .penalize_hard(lambda user: 100) # Assign a penalty of 100 for each detected user. + ) + +def run_simulation(): + # --- 1. Setup --- + builder = ConstraintBuilder(name="SecurityRules", score_class=HardSoftScore) + define_security_constraints(builder) + session = builder.build() + + # --- 2. Test Data --- + base_time = datetime(2025, 7, 15, 12, 0, 0) + + # Scenario 1: Malicious user 'intruder' + # This sequence should trigger the rule. + attack_sequence = [ + LoginAttempt(user='intruder', ip_address='1.1.1.1', timestamp=base_time, successful=False), + LoginAttempt(user='intruder', ip_address='2.2.2.2', timestamp=base_time + timedelta(minutes=1), successful=True), + FileAccess(user='intruder', file_path='/etc/shadow', operation='read', timestamp=base_time + timedelta(minutes=3),), + NetworkConnection(user='intruder', dest_ip='3.3.3.3', dest_port=6667, timestamp=base_time + timedelta(minutes=4)), + ] + + # Scenario 2: Admin user 's_admin' performs similar actions + # This should NOT trigger the rule due to the 'if_not_exists' check. + admin_actions = [ + LoginAttempt(user='s_admin', ip_address='10.0.0.1', timestamp=base_time + timedelta(hours=1), successful=False), + LoginAttempt(user='s_admin', ip_address='10.0.0.2', timestamp=base_time + timedelta(hours=1, minutes=1), successful=True), + FileAccess(user='s_admin', file_path='/etc/hosts', operation='write', timestamp=base_time + timedelta(hours=1, minutes=2)), + NetworkConnection(user='s_admin', dest_ip='8.8.8.8', dest_port=6667, timestamp=base_time + timedelta(hours=1, minutes=3)), + ] + + # --- 3. Execution --- + session.insert_batch([AdminUser(username='s_admin')]) # Add the admin to the session + session.insert_batch(attack_sequence) + session.insert_batch(admin_actions) + + score = session.get_score() + matches = session.get_constraint_matches() + + # --- 4. Display Results --- + print("## Simulation Results\n") + print(f"**Final Score:** {score}\n") + + if "anomalous_access_and_exfiltration" in matches: + print("**Detected Violations for 'anomalous_access_and_exfiltration':**\n") + + for score_obj, tuple_obj in matches["anomalous_access_and_exfiltration"]: + offending_user = tuple_obj.fact_a + print(f"- **User:** `{offending_user}`") + print(f"- **Penalty:** `{score_obj.simple_value}`") + print(" - **Reason:** This user, who is not an administrator, performed a sequence of actions matching the attack pattern.") + else: + print("**No violations detected.**") + + +if __name__ == "__main__": + run_simulation() diff --git a/examples/object_oriented/nqueens/cotwin/CotQueen.py b/examples/object_oriented/nqueens/cotwin/CotQueen.py index 30c760f..62a08b9 100644 --- a/examples/object_oriented/nqueens/cotwin/CotQueen.py +++ b/examples/object_oriented/nqueens/cotwin/CotQueen.py @@ -1,8 +1,13 @@ +from greyjack.score_calculation.greynet.greynet_fact import * +@greynet_fact class CotQueen(): def __init__(self, queen_id, row_id, column_id): self.queen_id = queen_id self.row_id = row_id self.column_id = column_id - pass \ No newline at end of file + pass + + def __repr__(self): + return str(self.column_id) + " | " + str(self.row_id) + " " + str(self.greynet_fact_id) \ No newline at end of file diff --git a/examples/object_oriented/nqueens/persistence/CotwinBuilderNQueens.py b/examples/object_oriented/nqueens/persistence/CotwinBuilderNQueens.py index ba20b26..e6ecc1f 100644 --- a/examples/object_oriented/nqueens/persistence/CotwinBuilderNQueens.py +++ b/examples/object_oriented/nqueens/persistence/CotwinBuilderNQueens.py @@ -5,13 +5,14 @@ from examples.object_oriented.nqueens.cotwin.NQueensCotwin import NQueensCotwin from examples.object_oriented.nqueens.score.PlainScoreCalculatorNQueens import PlainScoreCalculatorNQueens from examples.object_oriented.nqueens.score.IncrementalScoreCalculatorNQueens import IncrementalScoreCalculatorNQueens +from examples.object_oriented.nqueens.score.GreynetScoreCalculatorNQueens import greynet_score_calculator_nqueens from examples.object_oriented.nqueens.cotwin.CotQueen import CotQueen from greyjack.variables.GJInteger import GJInteger class CotwinBuilderNQueens(CotwinBuilderBase): - def __init__(self, use_incremental_score_calculator): - self.use_incremental_score_calculator = use_incremental_score_calculator + def __init__(self, scorer_name): + self.scorer_name = scorer_name pass def build_cotwin(self, domain_model, is_already_initialized): @@ -25,14 +26,19 @@ def build_cotwin(self, domain_model, is_already_initialized): column_id = i planning_row_id = GJInteger(0, n-1, False, queens[i].row.row_id, None) cot_queen = CotQueen( queen_id, planning_row_id, column_id ) + cot_queen.greynet_fact_id = queen_id cot_queens.append( cot_queen ) nqueens_cotwin = NQueensCotwin() nqueens_cotwin.add_planning_entities_list( cot_queens, "queens" ) - if self.use_incremental_score_calculator: + if self.scorer_name == "plain": + nqueens_cotwin.set_score_calculator( PlainScoreCalculatorNQueens() ) + elif self.scorer_name == "pseudo": nqueens_cotwin.set_score_calculator( IncrementalScoreCalculatorNQueens() ) + elif self.scorer_name == "greynet": + nqueens_cotwin.set_score_calculator( greynet_score_calculator_nqueens ) else: - nqueens_cotwin.set_score_calculator( PlainScoreCalculatorNQueens() ) + raise ValueError("Available score calculators: plain, pseudo, greynet") return nqueens_cotwin diff --git a/examples/object_oriented/nqueens/score/GreynetScoreCalculatorNQueens.py b/examples/object_oriented/nqueens/score/GreynetScoreCalculatorNQueens.py new file mode 100644 index 0000000..58b18a4 --- /dev/null +++ b/examples/object_oriented/nqueens/score/GreynetScoreCalculatorNQueens.py @@ -0,0 +1,35 @@ +# file: nqueens_constraint_builder.py + +from greyjack.score_calculation.score_calculators.GreynetScoreCalculator import GreynetScoreCalculator +from greyjack.score_calculation.greynet.builder import ConstraintBuilder +from greyjack.score_calculation.scores.SimpleScore import SimpleScore +from greyjack.score_calculation.scores.ScoreVariants import ScoreVariants +from ..cotwin.CotQueen import CotQueen + +cb = ConstraintBuilder(name="NQueens", score_class=SimpleScore) + +@cb.constraint("Row Conflict", default_weight=1.0) +def row_conflict(): + return ( + cb.for_each_unique_pair(CotQueen) + .filter(lambda q1, q2: q1.row_id == q2.row_id) + .penalize_simple(1) + ) + +@cb.constraint("Ascending Diagonal Conflict", default_weight=1.0) +def ascending_diagonal_conflict(): + return ( + cb.for_each_unique_pair(CotQueen) + .filter(lambda q1, q2: (q1.row_id - q1.column_id) == (q2.row_id - q2.column_id)) + .penalize_simple(1) + ) + +@cb.constraint("Descending Diagonal Conflict", default_weight=1.0) +def descending_diagonal_conflict(): + return ( + cb.for_each_unique_pair(CotQueen) + .filter(lambda q1, q2: (q1.row_id + q1.column_id) == (q2.row_id + q2.column_id)) + .penalize_simple(1) + ) + +greynet_score_calculator_nqueens = GreynetScoreCalculator(constraint_builder=cb, score_variant=ScoreVariants.SimpleScore) \ No newline at end of file diff --git a/examples/object_oriented/nqueens/scripts/solve_nqueens.py b/examples/object_oriented/nqueens/scripts/solve_nqueens.py deleted file mode 100644 index ce7aad0..0000000 --- a/examples/object_oriented/nqueens/scripts/solve_nqueens.py +++ /dev/null @@ -1,53 +0,0 @@ -from pathlib import Path -import os -import sys - -# To launch normally from console -script_dir_path = Path(os.path.dirname(os.path.realpath(__file__))) -project_dir_id = script_dir_path.parts.index("greyjack-solver-python") -project_dir_path = Path(*script_dir_path.parts[:project_dir_id+1]) -sys.path.append(str(project_dir_path)) - -from examples.object_oriented.nqueens.persistence.DomainBuilderNQueens import DomainBuilderNQueens -from examples.object_oriented.nqueens.persistence.CotwinBuilderNQueens import CotwinBuilderNQueens -from greyjack.agents.termination_strategies import * -from greyjack.agents import * -from greyjack.SolverOOP import SolverOOP -from greyjack.agents.base.LoggingLevel import LoggingLevel -from greyjack.agents.base.ParallelizationBackend import ParallelizationBackend -from greyjack.agents import * - -if __name__ == "__main__": - - # build domain model - domain_builder = DomainBuilderNQueens(10000, random_seed=45) - cotwin_builder = CotwinBuilderNQueens(use_incremental_score_calculator=True) - - #termination_strategy = StepsLimit(step_count_limit=1000) - #termination_strategy = TimeSpentLimit(time_seconds_limit=60) - #termination_strategy = ScoreNoImprovement(time_seconds_limit=15) - termination_strategy = ScoreLimit(score_to_compare=[0]) - agent = TabuSearch(neighbours_count=20, tabu_entity_rate=0.0, - mutation_rate_multiplier=None, move_probas=[0, 1, 0, 0, 0, 0], - migration_frequency=10, termination_strategy=termination_strategy) - """agent = GeneticAlgorithm(population_size=128, crossover_probability=0.5, p_best_rate=0.05, - tabu_entity_rate=0.0, mutation_rate_multiplier=1.0, move_probas=[0, 1, 0, 0, 0, 0], - migration_rate=0.00001, migration_frequency=1, termination_strategy=termination_strategy)""" - """agent = LateAcceptance(late_acceptance_size=10, tabu_entity_rate=0.0, - mutation_rate_multiplier=None, move_probas=[0, 1, 0, 0, 0, 0], - compare_to_global_frequency=1, termination_strategy=termination_strategy)""" - """agent = SimulatedAnnealing(initial_temperature=[1.0], cooling_rate=0.9999, tabu_entity_rate=0.0, - mutation_rate_multiplier=None, move_probas=[0, 1, 0, 0, 0, 0], - migration_frequency=10, compare_to_global_frequency=10, termination_strategy=termination_strategy)""" - - solver = SolverOOP(domain_builder, cotwin_builder, agent, - ParallelizationBackend.Multiprocessing, LoggingLevel.FreshOnly, - n_jobs=10, score_precision=[0]) - solution = solver.solve() - #print( "Cotwin solution looks that: " ) - #print( solution ) - - domain = domain_builder.build_from_solution(solution) - print(domain) - - print( "done" ) \ No newline at end of file diff --git a/examples/object_oriented/nqueens/scripts/solve_nqueens_fast.py b/examples/object_oriented/nqueens/scripts/solve_nqueens_fast.py new file mode 100644 index 0000000..a89bd37 --- /dev/null +++ b/examples/object_oriented/nqueens/scripts/solve_nqueens_fast.py @@ -0,0 +1,57 @@ +from pathlib import Path +import os +import sys + +# To launch normally from console +script_dir_path = Path(os.path.dirname(os.path.realpath(__file__))) +project_dir_id = script_dir_path.parts.index("greyjack-solver-python") +project_dir_path = Path(*script_dir_path.parts[:project_dir_id+1]) +sys.path.append(str(project_dir_path)) + +from examples.object_oriented.nqueens.persistence.DomainBuilderNQueens import DomainBuilderNQueens +from examples.object_oriented.nqueens.persistence.CotwinBuilderNQueens import CotwinBuilderNQueens +from greyjack.agents.termination_strategies import * +from greyjack.agents import * +from greyjack.SolverOOP import SolverOOP +from greyjack.agents.base.LoggingLevel import LoggingLevel +from greyjack.agents.base.ParallelizationBackend import ParallelizationBackend +from greyjack.agents import * +import traceback + +if __name__ == "__main__": + + try: + # build domain model + domain_builder = DomainBuilderNQueens(10000, random_seed=45) + cotwin_builder = CotwinBuilderNQueens(scorer_name="pseudo") + + #termination_strategy = StepsLimit(step_count_limit=1000) + #termination_strategy = TimeSpentLimit(time_seconds_limit=60) + #termination_strategy = ScoreNoImprovement(time_seconds_limit=15) + termination_strategy = ScoreLimit(score_to_compare=[0]) + agent = TabuSearch(neighbours_count=20, tabu_entity_rate=0.0, + mutation_rate_multiplier=None, move_probas=[0, 1, 0, 0, 0, 0], + migration_frequency=10, termination_strategy=termination_strategy) + """agent = GeneticAlgorithm(population_size=128, crossover_probability=0.5, p_best_rate=0.05, + tabu_entity_rate=0.0, mutation_rate_multiplier=1.0, move_probas=[0, 1, 0, 0, 0, 0], + migration_rate=0.00001, migration_frequency=1, termination_strategy=termination_strategy)""" + """agent = LateAcceptance(late_acceptance_size=10, tabu_entity_rate=0.0, + mutation_rate_multiplier=None, move_probas=[0, 1, 0, 0, 0, 0], + compare_to_global_frequency=1, termination_strategy=termination_strategy)""" + """agent = SimulatedAnnealing(initial_temperature=[1.0], cooling_rate=0.9999, tabu_entity_rate=0.0, + mutation_rate_multiplier=None, move_probas=[0, 1, 0, 0, 0, 0], + migration_frequency=10, compare_to_global_frequency=10, termination_strategy=termination_strategy)""" + + solver = SolverOOP(domain_builder, cotwin_builder, agent, + ParallelizationBackend.Multiprocessing, LoggingLevel.FreshOnly, + n_jobs=10, score_precision=[0]) + solution = solver.solve() + #print( "Cotwin solution looks that: " ) + #print( solution ) + + domain = domain_builder.build_from_solution(solution) + print(domain) + except Exception as e: + print(traceback.format_exc()) + + print( "done" ) \ No newline at end of file diff --git a/examples/object_oriented/nqueens/scripts/solve_nqueens_greynet_experimental.py b/examples/object_oriented/nqueens/scripts/solve_nqueens_greynet_experimental.py new file mode 100644 index 0000000..9e43b07 --- /dev/null +++ b/examples/object_oriented/nqueens/scripts/solve_nqueens_greynet_experimental.py @@ -0,0 +1,60 @@ +from pathlib import Path +import os +import sys + +# To launch normally from console +script_dir_path = Path(os.path.dirname(os.path.realpath(__file__))) +project_dir_id = script_dir_path.parts.index("greyjack-solver-python") +project_dir_path = Path(*script_dir_path.parts[:project_dir_id+1]) +sys.path.append(str(project_dir_path)) + +from examples.object_oriented.nqueens.persistence.DomainBuilderNQueens import DomainBuilderNQueens +from examples.object_oriented.nqueens.persistence.CotwinBuilderNQueens import CotwinBuilderNQueens +from greyjack.agents.termination_strategies import * +from greyjack.agents import * +from greyjack.SolverOOP import SolverOOP +from greyjack.agents.base.LoggingLevel import LoggingLevel +from greyjack.agents.base.ParallelizationBackend import ParallelizationBackend +from greyjack.agents import * +import traceback + +if __name__ == "__main__": + + try: + # build domain model + domain_builder = DomainBuilderNQueens(8, random_seed=45) + cotwin_builder = CotwinBuilderNQueens(scorer_name="greynet") + + #domain = domain_builder.build_domain_from_scratch() + #print(domain) + + #termination_strategy = StepsLimit(step_count_limit=1000) + #termination_strategy = TimeSpentLimit(time_seconds_limit=60) + #termination_strategy = ScoreNoImprovement(time_seconds_limit=15) + termination_strategy = ScoreLimit(score_to_compare=[0]) + agent = TabuSearch(neighbours_count=1, tabu_entity_rate=0.0, + mutation_rate_multiplier=None, move_probas=[0, 1, 0, 0, 0, 0], + migration_frequency=9999999999999, termination_strategy=termination_strategy) + """agent = GeneticAlgorithm(population_size=128, crossover_probability=0.5, p_best_rate=0.05, + tabu_entity_rate=0.0, mutation_rate_multiplier=1.0, move_probas=[0, 1, 0, 0, 0, 0], + migration_rate=0.00001, migration_frequency=1, termination_strategy=termination_strategy)""" + """agent = LateAcceptance(late_acceptance_size=10, tabu_entity_rate=0.0, + mutation_rate_multiplier=None, move_probas=[0, 1, 0, 0, 0, 0], + compare_to_global_frequency=1, termination_strategy=termination_strategy)""" + """agent = SimulatedAnnealing(initial_temperature=[1.0], cooling_rate=0.9999, tabu_entity_rate=0.0, + mutation_rate_multiplier=None, move_probas=[0, 1, 0, 0, 0, 0], + migration_frequency=10, compare_to_global_frequency=10, termination_strategy=termination_strategy)""" + + solver = SolverOOP(domain_builder, cotwin_builder, agent, + ParallelizationBackend.Threading, LoggingLevel.Info, + n_jobs=1, score_precision=[0]) + solution = solver.solve() + #print( "Cotwin solution looks that: " ) + #print( solution ) + + domain = domain_builder.build_from_solution(solution) + print(domain) + except Exception as e: + print(traceback.format_exc()) + + print( "done" ) \ No newline at end of file diff --git a/greyjack/Cargo.lock b/greyjack/Cargo.lock index 18724e8..2d4e0a1 100644 --- a/greyjack/Cargo.lock +++ b/greyjack/Cargo.lock @@ -579,7 +579,7 @@ checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" [[package]] name = "greyjack" -version = "0.2.6" +version = "0.3.3" dependencies = [ "chrono", "ndarray", diff --git a/greyjack/Cargo.toml b/greyjack/Cargo.toml index 9870a59..22ba600 100644 --- a/greyjack/Cargo.toml +++ b/greyjack/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "greyjack" -version = "0.2.6" +version = "0.3.3" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -22,8 +22,8 @@ polars = { version = "0.46.0", features = ["lazy", "ndarray", "serde", "abs"] } # if you build lib from source code # uncomment to gain max performance (increases calculation speed about ~10%, # but also increases build time ~20x times) -[profile.release] -lto = true -codegen-units = 1 -debug = true -opt-level = 3 +#[profile.release] +#lto = true +#codegen-units = 1 +#debug = true +#opt-level = 3 diff --git a/greyjack/greyjack/agents/TabuSearch.py b/greyjack/greyjack/agents/TabuSearch.py index 28e3244..c0f7914 100644 --- a/greyjack/greyjack/agents/TabuSearch.py +++ b/greyjack/greyjack/agents/TabuSearch.py @@ -31,17 +31,15 @@ def __init__( self.is_win_from_comparing_with_global = True def _build_metaheuristic_base(self): - - # when I use issubclass() solver dies silently, so check specific attributes if hasattr(self.cotwin, "planning_entities"): self.score_requester = OOPScoreRequester(self.cotwin) score_variant = self.cotwin.score_calculator.score_variant elif isinstance(self.cotwin, MathModel): self.score_requester = PureMathScoreRequester(self.cotwin) score_variant = self.cotwin.score_variant - self.cotwin.score_calculator.is_incremental = False # if True, currently works badder. Will try improve later + self.cotwin.score_calculator.is_incremental = False else: - raise Exception("Cotwin must be either subclass of CotwinBase, either be instance of MathModel") + raise Exception("Cotwin must be either subclass of CotwinBase, or an instance of MathModel") semantic_groups_dict = self.score_requester.variables_manager.semantic_groups_map.copy() discrete_ids = self.score_requester.variables_manager.discrete_ids @@ -57,7 +55,6 @@ def _build_metaheuristic_base(self): discrete_ids, ) - # to remove redundant clonning self.metaheuristic_name = self.metaheuristic_base.metaheuristic_name self.metaheuristic_kind = self.metaheuristic_base.metaheuristic_kind diff --git a/greyjack/greyjack/agents/base/Agent.py b/greyjack/greyjack/agents/base/Agent.py index 2fe6fb4..2c2da53 100644 --- a/greyjack/greyjack/agents/base/Agent.py +++ b/greyjack/greyjack/agents/base/Agent.py @@ -229,6 +229,19 @@ def _init_population(self): scores = self.score_requester.request_score_incremental(generated_sample, deltas) self.population.append(self.individual_type(generated_sample, scores[0])) + #if self.score_requester.is_greynet: + + ################## + # TODO: understand, why produces incorrect results + #self.score_requester.cotwin.score_calculator.commit_deltas(deltas[0]) + + #self.score_requester.cotwin.score_calculator._apply_deltas_internal(deltas[0]) + #self.score_requester.cotwin.score_calculator.update_entity_mapping_incremental(deltas[0]) + #new_score = self.score_requester.cotwin.score_calculator.get_score() + #self.population[0] = self.individual_type(self.population[0].variable_values, new_score) + ################## + + def _step_plain(self): new_population = [] samples = self.metaheuristic_base.sample_candidates_plain(self.population, self.agent_top_individual) @@ -240,6 +253,9 @@ def _step_plain(self): candidates = [self.individual_type(samples[i].copy(), scores[i]) for i in range(len(samples))] new_population = self.metaheuristic_base.build_updated_population(self.population, candidates) + #if self.score_requester.is_greynet: + # self.score_requester.cotwin.score_calculator.update_entity_mapping_plain(new_population[0].variable_values) + self.population = new_population def _step_incremental(self): @@ -250,7 +266,33 @@ def _step_incremental(self): for score in scores: score.round(self.score_precision) - new_population = self.metaheuristic_base.build_updated_population_incremental(self.population, sample, deltas, scores) + new_population, new_values = self.metaheuristic_base.build_updated_population_incremental(self.population, sample, deltas, scores) + if self.score_requester.is_greynet and new_values is not None: + + ################## + # TODO: understand, why produces incorrect results + #self.score_requester.cotwin.score_calculator.commit_deltas(new_values) + + #self.score_requester.cotwin.score_calculator._apply_deltas_internal(new_values) + #self.score_requester.cotwin.score_calculator.update_entity_mapping_incremental(new_values) + #new_score = self.score_requester.cotwin.score_calculator.get_score() + #new_population[0] = self.individual_type(new_population[0].variable_values, new_score) + ################## + + ################## + # gives correct results, but lacks of performance due to linear updates for each acceptable solution + self.score_requester.cotwin.score_calculator._apply_deltas_internal(list(enumerate(new_population[0].variable_values))) + new_score = self.score_requester.cotwin.score_calculator.get_score() + new_population[0] = self.individual_type(new_population[0].variable_values, new_score) + ################## + + #new_score = self.score_requester.cotwin.score_calculator._full_sync_and_get_score(new_population[0].variable_values) + #new_population[0] = self.individual_type(new_population[0].variable_values, new_score) + + #self.score_requester.cotwin.score_calculator._apply_deltas_internal(new_values) + #new_score = self.score_requester.cotwin.score_calculator.get_score() + #new_population[0] = self.individual_type(new_population[0].variable_values, new_score) + self.population = new_population def _send_receive_updates(self): @@ -456,6 +498,11 @@ def _get_updates_linux(self): self.population[:n_migrants] = updated_tail else: raise Exception("metaheuristic_kind can be only Population or LocalSearch") + + #if self.score_requester.is_greynet: + # self.score_requester.cotwin.score_calculator._full_sync_and_get_score(new_population[0].variable_values) + #if self.score_requester.is_greynet: + # self.score_requester.cotwin.score_calculator.update_entity_mapping_plain(self.population[0].variable_values) self.round_robin_status_dict = updates_reply["round_robin_status_dict"] self.round_robin_status_dict[self.agent_id] = self.agent_status @@ -496,6 +543,10 @@ def _check_global_updates_universal(self): if global_top_individual < self.agent_top_individual: self.agent_top_individual = global_top_individual self.population[0] = global_top_individual.copy() + #if self.score_requester.is_greynet: + # self.score_requester.cotwin.score_calculator._full_sync_and_get_score(new_population[0].variable_values) + #if self.score_requester.is_greynet: + # self.score_requester.cotwin.score_calculator.update_entity_mapping_plain(self.population[0].variable_values) is_variable_names_received = master_publication[1] self.is_master_received_variables_info = is_variable_names_received diff --git a/greyjack/greyjack/score_calculation/greynet/__init__.py b/greyjack/greyjack/score_calculation/greynet/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/greyjack/greyjack/score_calculation/greynet/builder.py b/greyjack/greyjack/score_calculation/greynet/builder.py new file mode 100644 index 0000000..42ac7ba --- /dev/null +++ b/greyjack/greyjack/score_calculation/greynet/builder.py @@ -0,0 +1,265 @@ +from __future__ import annotations +from typing import Type, Callable +from datetime import timedelta, datetime + +from .session import Session +from .streams.abstract_stream import AbstractStream +from .streams.stream import Stream +from .core.tuple import UniTuple, BiTuple +from .streams.scoring_stream import ScoringStream +from .constraint_factory import ConstraintFactory +from .common.joiner_type import JoinerType +from .function import Function +from .constraint import Constraint +from .constraint_weights import ConstraintWeights +from greyjack.score_calculation.scores.SimpleScore import SimpleScore + +from .collectors.count_collector import CountCollector +from .collectors.sum_collector import SumCollector +from .collectors.list_collector import ListCollector +from .collectors.set_collector import SetCollector +from .collectors.distinct_collector import DistinctCollector +from .collectors.min_collector import MinCollector +from .collectors.max_collector import MaxCollector +from .collectors.avg_collector import AvgCollector +from .collectors.stddev_collector import StdDevCollector +from .collectors.variance_collector import VarianceCollector +from .collectors.composite_collector import CompositeCollector +from .collectors.mapping_collector import MappingCollector +from .collectors.filtering_collector import FilteringCollector +from .collectors.constraint_match_collector import ConstraintMatchCollector + + +from .constraint_tools.consecutive_set_tree import ConsecutiveSetTree +from .constraint_tools.connected_range_tracker import ConnectedRangeTracker +from .constraint_tools.counting_bloom_filter import CountingBloomFilter + + + +class ConstraintBuilder: + def __init__(self, name: str = "default", score_class: Type = None, weights: ConstraintWeights = None): + if score_class is None: + score_class = SimpleScore + + self.factory = ConstraintFactory(name, score_class) + self.score_class = score_class + self.weights = weights if weights is not None else ConstraintWeights() + + if not hasattr(score_class, 'get_score_fields'): + raise TypeError(f"The score class '{score_class.__name__}' must have a " + "static method called 'get_score_fields' that returns a list of its score field names.") + + self.score_fields = score_class.get_score_fields() + + def constraint(self, constraint_id: str, default_weight: float = 1.0): + def decorator(func: Callable): + self.weights.set_weight(constraint_id, default_weight) + + def constraint_def() -> ScoringStream: + constraint_obj = func() + + if not isinstance(constraint_obj, Constraint): + raise TypeError(f"The function decorated by @constraint for '{constraint_id}' must end " + "with a call to a penalize method (e.g., .penalize_hard(...)).") + + constraint_obj.constraint_id = constraint_id + + target_field = f"{constraint_obj.score_type}_score" + if constraint_obj.score_type == "simple": + target_field = "simple_value" + + if target_field not in self.score_fields: + valid_types = [s.replace('_score', '').replace('_value', '') for s in self.score_fields] + raise ValueError(f"Score type '{constraint_obj.score_type}' is not valid for score class " + f"'{self.score_class.__name__}'. Valid types are: {valid_types}") + + def impact_function(*facts): + base_penalty = constraint_obj.penalty_function(*facts) + dynamic_weight = self.weights.get_weight(constraint_id) + final_penalty = float(base_penalty) * dynamic_weight + + score_kwargs = {field: 0.0 for field in self.score_fields} + score_kwargs[target_field] = abs(final_penalty) + return self.score_class(**score_kwargs) + + final_stream = ScoringStream( + source_stream=constraint_obj.stream, + constraint_id=constraint_obj.constraint_id, + impact_function=impact_function + ) + return final_stream + + self.factory.add_constraint(constraint_def) + return func + return decorator + + def for_each(self, fact_class) -> Stream[UniTuple]: + """Starts a stream from a given data class.""" + return self.factory.from_(fact_class) + + def for_each_unique_pair(self, fact_class) -> Stream[BiTuple]: + stream_1 = self.for_each(fact_class) + stream_2 = self.for_each(fact_class) + + return stream_1.join(stream_2, + JoinerType.LESS_THAN, + lambda fact: fact.greynet_fact_id, + lambda fact: fact.greynet_fact_id) + + + def build(self, **kwargs) -> Session: + """ + Builds and returns the session. + + Args: + debug (bool): If True, enables continuous tracing for the session's lifetime. + **kwargs: Additional arguments for the session. + """ + return self.factory.build_session(weights=self.weights, **kwargs) + + +class Collectors: + """A namespace for convenient access to collector suppliers.""" + @staticmethod + def count(): + return CountCollector + + @staticmethod + def sum(mapping_function): + class Wrapper(Function): + def apply(self, value): return mapping_function(value) + return lambda: SumCollector(Wrapper()) + + @staticmethod + def min(mapping_function: Callable): + """Creates a collector that finds the minimum value from a group.""" + class Wrapper(Function): + def apply(self, value): return mapping_function(value) + return lambda: MinCollector(Wrapper()) + + @staticmethod + def max(mapping_function: Callable): + """Creates a collector that finds the maximum value from a group.""" + class Wrapper(Function): + def apply(self, value): return mapping_function(value) + return lambda: MaxCollector(Wrapper()) + + @staticmethod + def avg(mapping_function: Callable): + """Creates a collector that calculates the average of a group.""" + class Wrapper(Function): + def apply(self, value): return mapping_function(value) + return lambda: AvgCollector(Wrapper()) + + @staticmethod + def stddev(mapping_function: Callable): + """Creates a collector that calculates the population standard deviation of a group.""" + class Wrapper(Function): + def apply(self, value): return mapping_function(value) + return lambda: StdDevCollector(Wrapper()) + + @staticmethod + def variance(mapping_function: Callable): + """Creates a collector that calculates the population variance of a group.""" + class Wrapper(Function): + def apply(self, value): return mapping_function(value) + return lambda: VarianceCollector(Wrapper()) + + @staticmethod + def compose(collector_suppliers: dict) -> Callable: + """ + Creates a composite collector to perform multiple aggregations at once. + """ + return lambda: CompositeCollector(collector_suppliers) + + @staticmethod + def to_list(): + return ListCollector + + @staticmethod + def to_set(): + return SetCollector + + @staticmethod + def distinct(): + return DistinctCollector + + @staticmethod + def mapping(mapping_function: Callable, downstream_supplier: Callable): + return lambda: MappingCollector(mapping_function, downstream_supplier) + + @staticmethod + def filtering(predicate: Callable, downstream_supplier: Callable): + return lambda: FilteringCollector(predicate, downstream_supplier) + + @staticmethod + def to_constraint_matches(): + """ + A semantic alias for to_list(), used for collecting the facts + that form a constraint match within a group_by operation. + """ + return ConstraintMatchCollector + + @staticmethod + def to_bloom_filter(estimated_items: int = 1000, false_positive_rate: float = 0.01): + """ + Creates a collector that aggregates items into a CountingBloomFilter. + """ + class BloomCollector: + def __init__(self): + self.bf = CountingBloomFilter(estimated_items, false_positive_rate) + + def insert(self, item): + self.bf.add(item) + return lambda: self.bf.remove(item) + + def result(self): + return self.bf + + def is_empty(self): + return len(self.bf) == 0 + return BloomCollector + + @staticmethod + def consecutive_sequences(sequence_func, increment_func=lambda p, i: p + i): + """ + Creates a collector that groups items into consecutive sequences. + """ + class Collector: + def __init__(self): self.tree = ConsecutiveSetTree(sequence_func, increment_func) + def insert(self, item): self.tree.add(item); return lambda: self.tree.remove(item) + def result(self): return self.tree.get_sequences() + def is_empty(self): return not self.tree.get_sequences() + return Collector + + @staticmethod + def connected_ranges(start_func, end_func): + """ + Creates a collector that groups items into connected (overlapping or adjacent) ranges. + """ + class Collector: + def __init__(self): self.tracker = ConnectedRangeTracker(start_func, end_func) + def insert(self, item): self.tracker.add(item); return lambda: self.tracker.remove(item) + def result(self): return self.tracker.get_connected_ranges() + def is_empty(self): return not self.tracker.get_connected_ranges() + return Collector + + +class Patterns: + """A helper class for defining common, complex constraint patterns.""" + def __init__(self, builder): + self.builder = builder + + def overlapping_ranges(self, fact_class, group_key_function, start_func, end_func): + """ + Creates a stream that finds pairs of facts of the same type that have + overlapping ranges, grouped by a key. + """ + return (self.builder.for_each(fact_class) + .join(self.builder.for_each(fact_class), + JoinerType.EQUAL, + group_key_function, + group_key_function) + .filter(lambda f1, f2: f1.greynet_fact_id < f2.greynet_fact_id) + .filter(lambda f1, f2: max(start_func(f1), start_func(f2)) < min(end_func(f1), end_func(f2))) + ) diff --git a/greyjack/greyjack/score_calculation/greynet/collectors/__init__.py b/greyjack/greyjack/score_calculation/greynet/collectors/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/greyjack/greyjack/score_calculation/greynet/collectors/avg_collector.py b/greyjack/greyjack/score_calculation/greynet/collectors/avg_collector.py new file mode 100644 index 0000000..2ea3f75 --- /dev/null +++ b/greyjack/greyjack/score_calculation/greynet/collectors/avg_collector.py @@ -0,0 +1,25 @@ + +# greynet/collectors/avg_collector.py + +from .base_collector import BaseCollector + +class AvgCollector(BaseCollector): + def __init__(self, mapping_function): + self._mapping_function = mapping_function + self._sum = 0.0 + self._count = 0 + + def insert(self, item): + value = float(self._mapping_function.apply(item)) + self._sum += value + self._count += 1 + def undo(): + self._sum -= value + self._count -= 1 + return undo + + def result(self): + return self._sum / self._count if self._count > 0 else 0.0 + + def is_empty(self): + return self._count == 0 diff --git a/greyjack/greyjack/score_calculation/greynet/collectors/base_collector.py b/greyjack/greyjack/score_calculation/greynet/collectors/base_collector.py new file mode 100644 index 0000000..2228bb3 --- /dev/null +++ b/greyjack/greyjack/score_calculation/greynet/collectors/base_collector.py @@ -0,0 +1,27 @@ +# greynet/collectors/base_collector.py +from abc import ABC, abstractmethod + +class BaseCollector(ABC): + @abstractmethod + def insert(self, item): + """ + Adds an item to the collection and returns an undo function. + + The undo function is critical for retract operations, allowing the + collector to precisely reverse the effect of an insertion. + """ + pass + + @abstractmethod + def result(self): + """ + Returns the current result of the aggregation. + """ + pass + + @abstractmethod + def is_empty(self): + """ + Returns True if the collection is empty, False otherwise. + """ + pass \ No newline at end of file diff --git a/greyjack/greyjack/score_calculation/greynet/collectors/composite_collector.py b/greyjack/greyjack/score_calculation/greynet/collectors/composite_collector.py new file mode 100644 index 0000000..bb102f7 --- /dev/null +++ b/greyjack/greyjack/score_calculation/greynet/collectors/composite_collector.py @@ -0,0 +1,63 @@ +# greynet/collectors/composite_collector.py +from .base_collector import BaseCollector + +class CompositeCollector(BaseCollector): + """ + A collector that wraps multiple collectors to perform several aggregations + on the same group simultaneously. + """ + def __init__(self, collector_suppliers: dict): + """ + Initializes the CompositeCollector. + + Args: + collector_suppliers (dict): A dictionary where keys are strings (the name + of the aggregation) and values are collector + suppliers (e.g., Collectors.count()). + """ + # Instantiate each collector from its supplier function + self._collectors = { + key: supplier() for key, supplier in collector_suppliers.items() + } + # Maps an item's memory ID to its list of undo functions from sub-collectors + self._undo_map = {} + + def insert(self, item): + item_id = item.greynet_fact_id + # It's possible for an item to be re-inserted in some complex update scenarios, + # so we ensure a clean list of undo functions. + self._undo_map[item_id] = [] + + for collector in self._collectors.values(): + undo_func = collector.insert(item) + self._undo_map[item_id].append(undo_func) + + def undo(): + """The undo function to be returned for this insertion.""" + if item_id in self._undo_map: + for single_undo_func in self._undo_map[item_id]: + single_undo_func() + del self._undo_map[item_id] + + return undo + + def result(self): + """ + Returns a dictionary containing the results from each nested collector. + """ + return { + key: collector.result() for key, collector in self._collectors.items() + } + + def is_empty(self): + """ + The composite is considered empty if its collectors have not processed any items. + We can check the state of the first collector as they are all populated in sync. + """ + if not self._collectors: + return True + + # Get the first collector instance to check its state + first_collector = next(iter(self._collectors.values())) + return first_collector.is_empty() + diff --git a/greyjack/greyjack/score_calculation/greynet/collectors/constraint_match_collector.py b/greyjack/greyjack/score_calculation/greynet/collectors/constraint_match_collector.py new file mode 100644 index 0000000..da1f9d0 --- /dev/null +++ b/greyjack/greyjack/score_calculation/greynet/collectors/constraint_match_collector.py @@ -0,0 +1,15 @@ +# greynet/collectors/constraint_match_collector.py +from __future__ import annotations +from .list_collector import ListCollector + +class ConstraintMatchCollector(ListCollector): + """ + A collector for tracking the specific facts that constitute a constraint match + within a group. It is functionally equivalent to a ListCollector but provides + semantic clarity in rule definitions. + + Note: The primary mechanism for tracking all constraint matches across the + entire session is the `session.get_constraint_matches()` method, which + inspects the internal state of the engine's ScoringNodes. + """ + pass diff --git a/greyjack/greyjack/score_calculation/greynet/collectors/count_collector.py b/greyjack/greyjack/score_calculation/greynet/collectors/count_collector.py new file mode 100644 index 0000000..4423978 --- /dev/null +++ b/greyjack/greyjack/score_calculation/greynet/collectors/count_collector.py @@ -0,0 +1,18 @@ +# greynet/collectors/count_collector.py +from ..collectors.base_collector import BaseCollector + +class CountCollector(BaseCollector): + def __init__(self): + self._count = 0 + + def insert(self, item): + self._count += 1 + def undo(): + self._count -= 1 + return undo + + def result(self): + return self._count + + def is_empty(self): + return self._count == 0 \ No newline at end of file diff --git a/greyjack/greyjack/score_calculation/greynet/collectors/distinct_collector.py b/greyjack/greyjack/score_calculation/greynet/collectors/distinct_collector.py new file mode 100644 index 0000000..d23c947 --- /dev/null +++ b/greyjack/greyjack/score_calculation/greynet/collectors/distinct_collector.py @@ -0,0 +1,35 @@ +# greynet/collectors/distinct_collector.py +from __future__ import annotations +from collections import Counter +from .base_collector import BaseCollector + +class DistinctCollector(BaseCollector): + """ + A collector that aggregates unique items into a list, preserving insertion order. + It correctly handles the insertion and retraction of duplicate items. + """ + def __init__(self): + self._items = {} # Using a dict as an ordered set + self._counter = Counter() + + def insert(self, item): + """Adds an item if it's not already present and tracks its reference count.""" + if self._counter[item] == 0: + self._items[item] = None # Add to the ordered set + self._counter[item] += 1 + + def undo(): + """Decrements the item's reference count and removes it if the count reaches zero.""" + self._counter[item] -= 1 + if self._counter[item] == 0: + self._items.pop(item, None) + del self._counter[item] + return undo + + def result(self): + """Returns a list of the unique items in their insertion order.""" + return list(self._items.keys()) + + def is_empty(self): + """Checks if the collection is empty.""" + return not self._items diff --git a/greyjack/greyjack/score_calculation/greynet/collectors/filtering_collector.py b/greyjack/greyjack/score_calculation/greynet/collectors/filtering_collector.py new file mode 100644 index 0000000..024f284 --- /dev/null +++ b/greyjack/greyjack/score_calculation/greynet/collectors/filtering_collector.py @@ -0,0 +1,35 @@ +# greynet/collectors/filtering_collector.py +from __future__ import annotations +from .base_collector import BaseCollector +from ..function import Predicate +from typing import Callable + +class FilteringCollector(BaseCollector): + """ + A collector that filters items based on a predicate before passing them + to a downstream collector for aggregation. + """ + def __init__(self, predicate: Callable, downstream_supplier: Callable): + if not isinstance(predicate, Predicate): + class Wrapper(Predicate): + def test(self, value): return predicate(value) + self._predicate = Wrapper() + else: + self._predicate = predicate + self._downstream = downstream_supplier() + + def insert(self, item): + """If the item passes the predicate, insert it into the downstream collector.""" + if self._predicate.test(item): + return self._downstream.insert(item) + else: + # Return a no-op undo function if the item is filtered out. + return lambda: None + + def result(self): + """Returns the result from the downstream collector.""" + return self._downstream.result() + + def is_empty(self): + """Checks if the downstream collector is empty.""" + return self._downstream.is_empty() diff --git a/greyjack/greyjack/score_calculation/greynet/collectors/list_collector.py b/greyjack/greyjack/score_calculation/greynet/collectors/list_collector.py new file mode 100644 index 0000000..4efebcf --- /dev/null +++ b/greyjack/greyjack/score_calculation/greynet/collectors/list_collector.py @@ -0,0 +1,27 @@ +# greynet/collectors/list_collector.py +from ..collectors.base_collector import BaseCollector + +class ListCollector(BaseCollector): + def __init__(self): + self._items = [] + + def insert(self, item): + self._items.append(item) + def undo(): + # Removing the specific item instance is crucial for correctness, + # especially if duplicate items can exist. + try: + self._items.remove(item) + except ValueError: + # This can happen in complex scenarios if an item is retracted + # more than once; it's safe to ignore. + pass + return undo + + def result(self): + # Return a copy to prevent external mutations from affecting the + # internal state of the collector. + return self._items.copy() + + def is_empty(self): + return not self._items \ No newline at end of file diff --git a/greyjack/greyjack/score_calculation/greynet/collectors/mapping_collector.py b/greyjack/greyjack/score_calculation/greynet/collectors/mapping_collector.py new file mode 100644 index 0000000..e5261cf --- /dev/null +++ b/greyjack/greyjack/score_calculation/greynet/collectors/mapping_collector.py @@ -0,0 +1,32 @@ +# greynet/collectors/mapping_collector.py +from __future__ import annotations +from .base_collector import BaseCollector +from ..function import Function +from typing import Callable + +class MappingCollector(BaseCollector): + """ + A collector that first applies a mapping function to each item before + passing it to a downstream collector for aggregation. + """ + def __init__(self, mapping_function: Callable, downstream_supplier: Callable): + if not isinstance(mapping_function, Function): + class Wrapper(Function): + def apply(self, value): return mapping_function(value) + self._mapping_function = Wrapper() + else: + self._mapping_function = mapping_function + self._downstream = downstream_supplier() + + def insert(self, item): + """Applies the mapping function and inserts the result into the downstream collector.""" + mapped_item = self._mapping_function.apply(item) + return self._downstream.insert(mapped_item) + + def result(self): + """Returns the result from the downstream collector.""" + return self._downstream.result() + + def is_empty(self): + """Checks if the downstream collector is empty.""" + return self._downstream.is_empty() diff --git a/greyjack/greyjack/score_calculation/greynet/collectors/max_collector.py b/greyjack/greyjack/score_calculation/greynet/collectors/max_collector.py new file mode 100644 index 0000000..4d077d4 --- /dev/null +++ b/greyjack/greyjack/score_calculation/greynet/collectors/max_collector.py @@ -0,0 +1,39 @@ +# FILE: greynet/collectors/max_collector.py +from __future__ import annotations +import bisect +from collections import Counter +from .base_collector import BaseCollector + +class MaxCollector(BaseCollector): + """ + A collector that efficiently finds the maximum value from a group. + It supports efficient insertion and retraction of items. + """ + def __init__(self, mapping_function): + self._mapping_function = mapping_function + self._counts = Counter() + self._sorted_keys = [] + + def insert(self, item): + value = self._mapping_function.apply(item) + + if self._counts[value] == 0: + bisect.insort_left(self._sorted_keys, value) + + self._counts[value] += 1 + + def undo(): + self._counts[value] -= 1 + if self._counts[value] == 0: + del self._counts[value] + key_index = bisect.bisect_left(self._sorted_keys, value) + if key_index < len(self._sorted_keys) and self._sorted_keys[key_index] == value: + self._sorted_keys.pop(key_index) + return undo + + def result(self): + """Returns the maximum value in O(1) time.""" + return self._sorted_keys[-1] if self._sorted_keys else None + + def is_empty(self): + return not self._sorted_keys diff --git a/greyjack/greyjack/score_calculation/greynet/collectors/min_collector.py b/greyjack/greyjack/score_calculation/greynet/collectors/min_collector.py new file mode 100644 index 0000000..a79ee87 --- /dev/null +++ b/greyjack/greyjack/score_calculation/greynet/collectors/min_collector.py @@ -0,0 +1,42 @@ +# FILE: greynet/collectors/min_collector.py +from __future__ import annotations +import bisect +from collections import Counter +from .base_collector import BaseCollector + +class MinCollector(BaseCollector): + """ + A collector that efficiently finds the minimum value from a group. + It supports efficient insertion and retraction of items. + """ + def __init__(self, mapping_function): + self._mapping_function = mapping_function + self._counts = Counter() + self._sorted_keys = [] + + def insert(self, item): + value = self._mapping_function.apply(item) + + if self._counts[value] == 0: + # Insert key into sorted list only if it's the first time we see it + bisect.insort_left(self._sorted_keys, value) + + self._counts[value] += 1 + + def undo(): + self._counts[value] -= 1 + if self._counts[value] == 0: + # Remove the key from the sorted list if no more items have this value + del self._counts[value] + key_index = bisect.bisect_left(self._sorted_keys, value) + if key_index < len(self._sorted_keys) and self._sorted_keys[key_index] == value: + self._sorted_keys.pop(key_index) + return undo + + def result(self): + """Returns the minimum value in O(1) time.""" + return self._sorted_keys[0] if self._sorted_keys else None + + def is_empty(self): + return not self._sorted_keys + diff --git a/greyjack/greyjack/score_calculation/greynet/collectors/set_collector.py b/greyjack/greyjack/score_calculation/greynet/collectors/set_collector.py new file mode 100644 index 0000000..5251081 --- /dev/null +++ b/greyjack/greyjack/score_calculation/greynet/collectors/set_collector.py @@ -0,0 +1,35 @@ +# greynet/collectors/set_collector.py +from __future__ import annotations +from collections import Counter +from .base_collector import BaseCollector + +class SetCollector(BaseCollector): + """ + A collector that aggregates items into a set, ensuring uniqueness. + It correctly handles the insertion and retraction of duplicate items. + """ + def __init__(self): + self._items = set() + self._counter = Counter() + + def insert(self, item): + """Adds an item to the set and tracks its reference count.""" + if self._counter[item] == 0: + self._items.add(item) + self._counter[item] += 1 + + def undo(): + """Decrements the item's reference count and removes it from the set if the count reaches zero.""" + self._counter[item] -= 1 + if self._counter[item] == 0: + self._items.discard(item) # Use discard for safe removal + del self._counter[item] + return undo + + def result(self): + """Returns a copy of the resulting set.""" + return self._items.copy() + + def is_empty(self): + """Checks if the collection is empty.""" + return not self._items diff --git a/greyjack/greyjack/score_calculation/greynet/collectors/stddev_collector.py b/greyjack/greyjack/score_calculation/greynet/collectors/stddev_collector.py new file mode 100644 index 0000000..0e6f683 --- /dev/null +++ b/greyjack/greyjack/score_calculation/greynet/collectors/stddev_collector.py @@ -0,0 +1,36 @@ +# greynet/collectors/.py +from .base_collector import BaseCollector +import math + +class StdDevCollector(BaseCollector): + def __init__(self, mapping_function): + self._mapping_function = mapping_function + self._sum = 0.0 + self._sum_sq = 0.0 # Sum of squares + self._count = 0 + + def insert(self, item): + value = float(self._mapping_function.apply(item)) + self._sum += value + self._sum_sq += value ** 2 + self._count += 1 + def undo(): + self._sum -= value + self._sum_sq -= value ** 2 + self._count -= 1 + return undo + + def result(self): + if self._count < 2: + return 0.0 # Standard deviation for 0 or 1 items is 0. + + mean = self._sum / self._count + # Population variance formula: E[X^2] - (E[X])^2 + variance = (self._sum_sq / self._count) - (mean ** 2) + + # Clamp variance at 0 to avoid domain errors with math.sqrt() + # due to potential floating-point inaccuracies. + return math.sqrt(max(0, variance)) + + def is_empty(self): + return self._count == 0 diff --git a/greyjack/greyjack/score_calculation/greynet/collectors/sum_collector.py b/greyjack/greyjack/score_calculation/greynet/collectors/sum_collector.py new file mode 100644 index 0000000..eaba425 --- /dev/null +++ b/greyjack/greyjack/score_calculation/greynet/collectors/sum_collector.py @@ -0,0 +1,23 @@ +# greynet/collectors/sum_collector.py +from ..collectors.base_collector import BaseCollector + +class SumCollector(BaseCollector): + def __init__(self, mapping_function): + self._mapping_function = mapping_function + self._total = 0 + self._count = 0 # Track item count to correctly handle emptiness + + def insert(self, item): + value = self._mapping_function.apply(item) + self._total += value + self._count += 1 + def undo(): + self._total -= value + self._count -= 1 + return undo + + def result(self): + return self._total + + def is_empty(self): + return self._count == 0 \ No newline at end of file diff --git a/greyjack/greyjack/score_calculation/greynet/collectors/temporal_collectors.py b/greyjack/greyjack/score_calculation/greynet/collectors/temporal_collectors.py new file mode 100644 index 0000000..6a31b3a --- /dev/null +++ b/greyjack/greyjack/score_calculation/greynet/collectors/temporal_collectors.py @@ -0,0 +1,180 @@ +from collections import defaultdict, deque +from datetime import datetime, timedelta, timezone +# --- Start of Bug Fix --- +from typing import Dict, List, Callable, Optional, Any, Tuple +# --- End of Bug Fix --- +from dataclasses import dataclass +from typing import Optional +from enum import Enum +import bisect + +# TODO: Fix temporal, sequential features +# TODO: Add debug, tracing for temporal, sequential features + +class WindowType(Enum): + TUMBLING = "tumbling" # Non-overlapping fixed windows + SLIDING = "sliding" # Overlapping windows that slide + SESSION = "session" # Dynamic windows based on activity gaps + HOPPING = "hopping" # Fixed-size windows with custom hop interval + +@dataclass(frozen=True) +class TimeWindow: + start: datetime + end: datetime + window_type: WindowType + + def contains(self, timestamp: datetime) -> bool: + return self.start <= timestamp < self.end + + def overlaps(self, other: 'TimeWindow') -> bool: + return not (self.end <= other.start or other.end <= self.start) + + def duration(self) -> timedelta: + return self.end - self.start + + @classmethod + def tumbling(cls, start: datetime, duration: timedelta) -> 'TimeWindow': + return cls(start, start + duration, WindowType.TUMBLING) + + @classmethod + def sliding(cls, start: datetime, duration: timedelta) -> 'TimeWindow': + return cls(start, start + duration, WindowType.SLIDING) + +@dataclass +class TemporalEvent: + """Wrapper for facts with temporal information""" + fact: Any + timestamp: datetime + event_id: Optional[str] = None + + def __post_init__(self): + if self.event_id is None: + self.event_id = f"evt_{self.fact.greynet_fact_id}_{self.timestamp.timestamp()}" + +# --- Start of Bug Fix --- +@dataclass() +class EventSequencePattern: + """ + ENHANCED: Improved pattern definition with better validation. + """ + pattern_steps: Tuple[Callable[[Any], bool], ...] + within: timedelta + allow_gaps: bool = True +# --- End of Bug Fix --- + + def __post_init__(self): + """Validate pattern configuration.""" + if not self.pattern_steps: + raise ValueError("Pattern must have at least one step") + + if len(self.pattern_steps) < 2: + raise ValueError("Pattern must have at least two steps to form a sequence") + + if self.within.total_seconds() <= 0: + raise ValueError("Time window must be positive") + + # Validate that all steps are callable + for i, step in enumerate(self.pattern_steps): + if not callable(step): + raise ValueError(f"Pattern step {i} must be callable") + + def matches_sequence(self, facts: List[Any], timestamps: List[datetime]) -> bool: + """ + ENHANCED: Validates that a sequence of facts matches this pattern. + """ + if len(facts) != len(self.pattern_steps): + return False + + if len(facts) != len(timestamps): + return False + + # Check time window constraint + if timestamps[-1] - timestamps[0] > self.within: + return False + + # Check each step matches + for i, (fact, predicate) in enumerate(zip(facts, self.pattern_steps)): + if not predicate(fact): + return False + + # Check temporal ordering (timestamps should be non-decreasing) + for i in range(1, len(timestamps)): + if timestamps[i] < timestamps[i-1]: + return False + + # If gaps are not allowed, check for strict temporal adjacency + if not self.allow_gaps: + # This would require domain-specific logic to determine + # what constitutes "adjacent" events + pass + + return True + +class TemporalCollector: + """Base class for temporal aggregation collectors""" + + def __init__(self, time_extractor: Callable[[Any], datetime]): + self.time_extractor = time_extractor + self.events: List[TemporalEvent] = [] + + def insert(self, item: Any): + timestamp = self.time_extractor(item) + event = TemporalEvent(item, timestamp) + + insert_pos = bisect.bisect_left(self.events, event.timestamp, key=lambda e: e.timestamp) + self.events.insert(insert_pos, event) + + def undo(): + try: + self.events.remove(event) + except ValueError: + pass + return undo + + def get_events_in_window(self, window: TimeWindow) -> List[Any]: + start_idx = bisect.bisect_left(self.events, window.start, key=lambda e: e.timestamp) + end_idx = bisect.bisect_right(self.events, window.end, key=lambda e: e.timestamp) + return [e.fact for e in self.events[start_idx:end_idx]] + +class TumblingWindowCollector(TemporalCollector): + """Collector that creates non-overlapping tumbling windows""" + + def __init__(self, + time_extractor: Callable[[Any], datetime], + window_size: timedelta, + window_start: Optional[datetime] = None): + super().__init__(time_extractor) + self.window_size = window_size + + if window_start: + self.window_start_epoch = window_start.timestamp() + else: + self.window_start_epoch = datetime(1970, 1, 1, tzinfo=timezone.utc).timestamp() + + self._windows: Dict[int, List[Any]] = defaultdict(list) + + def _get_window_key(self, timestamp: datetime) -> int: + elapsed = timestamp.timestamp() - self.window_start_epoch + window_size_sec = self.window_size.total_seconds() + window_index = int(elapsed // window_size_sec) + return int(self.window_start_epoch + window_index * window_size_sec) + + def insert(self, item: Any): + undo_super = super().insert(item) + self._rebuild_windows() + def undo(): + undo_super() + self._rebuild_windows() + return undo + + def _rebuild_windows(self): + self._windows.clear() + for event in self.events: + window_key = self._get_window_key(event.timestamp) + self._windows[window_key].append(event.fact) + + def result(self) -> Dict[datetime, List[Any]]: + return {datetime.fromtimestamp(k, tz=timezone.utc): v for k, v in self._windows.items()} + + def is_empty(self) -> bool: + return not self.events diff --git a/greyjack/greyjack/score_calculation/greynet/collectors/variance_collector.py b/greyjack/greyjack/score_calculation/greynet/collectors/variance_collector.py new file mode 100644 index 0000000..0f4e75d --- /dev/null +++ b/greyjack/greyjack/score_calculation/greynet/collectors/variance_collector.py @@ -0,0 +1,43 @@ +# greynet/collectors/variance_collector.py +from __future__ import annotations +from .base_collector import BaseCollector +import math + +class VarianceCollector(BaseCollector): + """ + A collector that calculates the population variance for a group of items + based on a numeric mapping function. + """ + def __init__(self, mapping_function): + self._mapping_function = mapping_function + self._sum = 0.0 + self._sum_sq = 0.0 # Sum of squares + self._count = 0 + + def insert(self, item): + """Adds a value to the calculation.""" + value = float(self._mapping_function.apply(item)) + self._sum += value + self._sum_sq += value ** 2 + self._count += 1 + def undo(): + self._sum -= value + self._sum_sq -= value ** 2 + self._count -= 1 + return undo + + def result(self): + """Returns the calculated population variance.""" + if self._count < 2: + return 0.0 + + mean = self._sum / self._count + # Population variance formula: E[X^2] - (E[X])^2 + variance = (self._sum_sq / self._count) - (mean ** 2) + + # Clamp variance at 0 to avoid domain errors from floating-point inaccuracies. + return max(0, variance) + + def is_empty(self): + """Checks if any items have been collected.""" + return self._count == 0 diff --git a/greyjack/greyjack/score_calculation/greynet/common/__init__.py b/greyjack/greyjack/score_calculation/greynet/common/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/greyjack/greyjack/score_calculation/greynet/common/index/__init__.py b/greyjack/greyjack/score_calculation/greynet/common/index/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/greyjack/greyjack/score_calculation/greynet/common/index/advanced_index.py b/greyjack/greyjack/score_calculation/greynet/common/index/advanced_index.py new file mode 100644 index 0000000..c3ec02b --- /dev/null +++ b/greyjack/greyjack/score_calculation/greynet/common/index/advanced_index.py @@ -0,0 +1,173 @@ + +# greynet/common/index/advanced_index.py + +from __future__ import annotations +import bisect +from collections import defaultdict +from typing import List, Any + +from ...common.joiner_type import JoinerType +from ...core.tuple import AbstractTuple +from ...constraint_tools.counting_bloom_filter import CountingBloomFilter + +class AdvancedIndex: + """ + An index that supports advanced join types, including range-based comparisons. + - For EQUAL, it uses a hash map for O(1) average time complexity. + - For NOT_EQUAL, it uses a hybrid strategy with a Counting Bloom Filter + and a flat list of all tuples to optimize lookups. + - For range comparisons (LESS_THAN, etc.), it maintains a sorted list. + """ + def __init__(self, index_properties, joiner_type: JoinerType = JoinerType.EQUAL): + self._index_properties = index_properties + self._joiner_type = joiner_type + + # Select the appropriate data structure based on the joiner type + if joiner_type == JoinerType.EQUAL: + self._index_map = defaultdict(list) + # --- Start of Modified Code --- + elif joiner_type == JoinerType.NOT_EQUAL: + # For NOT_EQUAL joins, we use a hybrid approach. + # 1. A standard hash map to quickly find tuples TO EXCLUDE. + self._index_map = defaultdict(list) + # 2. A flat list of all tuples for faster full iteration than dict.keys(). + self._all_tuples: List[AbstractTuple] = [] + # 3. A Counting Bloom Filter for a fast probabilistic check. + # These values are defaults; could be made configurable. + self._bloom_filter = CountingBloomFilter(estimated_items=1000, false_positive_rate=0.01) + # --- End of Modified Code --- + else: + # Sorted list for efficient range scans + self._sorted_entries: List[tuple[Any, List[AbstractTuple]]] = [] + self._keys_view: List[Any] = [] # A synchronized view of keys for bisect + + def put(self, tuple_: AbstractTuple): + """Adds a tuple to the index.""" + key = self._index_properties.get_property(tuple_) + # --- Start of Modified Code --- + if self._joiner_type == JoinerType.NOT_EQUAL: + self._index_map[key].append(tuple_) + self._all_tuples.append(tuple_) + # Add key to bloom filter only if it's the first time we see this key. + if len(self._index_map[key]) == 1: + self._bloom_filter.add(key) + # --- End of Modified Code --- + elif hasattr(self, '_index_map'): + self._index_map[key].append(tuple_) + else: + # Find the insertion point in the sorted list + idx = bisect.bisect_left(self._keys_view, key) + + # If a list of tuples already exists for this key, append to it + if idx < len(self._keys_view) and self._keys_view[idx] == key: + self._sorted_entries[idx][1].append(tuple_) + else: + # Otherwise, insert a new (key, [tuple]) entry + self._sorted_entries.insert(idx, (key, [tuple_])) + self._keys_view.insert(idx, key) + + def get_matches(self, query_key: Any) -> List[AbstractTuple]: + """ + Retrieves all tuples that match the query_key according to the joiner type. + """ + if hasattr(self, '_index_map'): + if self._joiner_type == JoinerType.EQUAL: + return self._index_map.get(query_key, []) + + # --- Start of Modified Code --- + if self._joiner_type == JoinerType.NOT_EQUAL: + # OPTIMIZATION 1: Use the Bloom filter for a fast path. + # If the key is definitively not in the index, no tuples need to be + # excluded, so we can return the entire list of tuples. + if query_key not in self._bloom_filter: + return self._all_tuples.copy() + + # OPTIMIZATION 2: The key might be present. + # We get the set of tuples to exclude. Using a set provides O(1) + # average time complexity for the 'in' check below. + tuples_to_exclude = set(self._index_map.get(query_key, [])) + + # If the Bloom filter had a false positive, the exclusion set will be empty. + if not tuples_to_exclude: + return self._all_tuples.copy() + + # Perform the full exclusion. This is still O(N), but iterating a list + # is generally faster than iterating dictionary items/keys. + return [t for t in self._all_tuples if t not in tuples_to_exclude] + # --- End of Modified Code --- + else: + # --- Start of Enhancement --- + # Use bisect for efficient O(log N) lookups on the sorted list + if self._joiner_type == JoinerType.LESS_THAN: + # Find index of first element >= query_key + end_idx = bisect.bisect_left(self._keys_view, query_key) + target_slice = self._sorted_entries[:end_idx] + elif self._joiner_type == JoinerType.LESS_THAN_OR_EQUAL: + # Find index of first element > query_key + end_idx = bisect.bisect_right(self._keys_view, query_key) + target_slice = self._sorted_entries[:end_idx] + elif self._joiner_type == JoinerType.GREATER_THAN: + # Find index of first element > query_key + start_idx = bisect.bisect_right(self._keys_view, query_key) + target_slice = self._sorted_entries[start_idx:] + elif self._joiner_type == JoinerType.GREATER_THAN_OR_EQUAL: + # Find index of first element >= query_key + start_idx = bisect.bisect_left(self._keys_view, query_key) + target_slice = self._sorted_entries[start_idx:] + else: + # Fallback for any other custom comparators, though less efficient. + # The primary range joins are now optimized. + comparator = self._joiner_type.create_comparator() + target_slice = [ + entry for entry in self._sorted_entries + if comparator(entry[0], query_key) + ] + + # Flatten the list of lists of tuples into a single list of tuples + return [ + tuple_ for _, tuple_list in target_slice for tuple_ in tuple_list + ] + # --- End of Enhancement --- + + def remove(self, tuple_: AbstractTuple): + """Removes a tuple from the index.""" + key = self._index_properties.get_property(tuple_) + # --- Start of Modified Code --- + if self._joiner_type == JoinerType.NOT_EQUAL: + if key in self._index_map: + try: + self._index_map[key].remove(tuple_) + # This is O(N) and a clear performance trade-off for this strategy. + self._all_tuples.remove(tuple_) + + # If the key is no longer associated with any tuples, + # remove it from the map and the Bloom filter. + if not self._index_map[key]: + del self._index_map[key] + self._bloom_filter.remove(key) + except ValueError: + pass # Tuple was already removed. + # --- End of Modified Code --- + elif hasattr(self, '_index_map'): + if key in self._index_map: + try: + self._index_map[key].remove(tuple_) + if not self._index_map[key]: + del self._index_map[key] + except ValueError: + # Tuple was already removed or never existed, ignore. + pass + else: + # Find the entry using binary search + idx = bisect.bisect_left(self._keys_view, key) + if idx < len(self._keys_view) and self._keys_view[idx] == key: + try: + tuples = self._sorted_entries[idx][1] + tuples.remove(tuple_) + # If the list for this key is now empty, remove the key itself + if not tuples: + self._sorted_entries.pop(idx) + self._keys_view.pop(idx) + except ValueError: + # Tuple was already removed, ignore. + pass diff --git a/greyjack/greyjack/score_calculation/greynet/common/index/uni_index.py b/greyjack/greyjack/score_calculation/greynet/common/index/uni_index.py new file mode 100644 index 0000000..f00ac8d --- /dev/null +++ b/greyjack/greyjack/score_calculation/greynet/common/index/uni_index.py @@ -0,0 +1,24 @@ +# greynet/common/index/uni_index.py +from collections import defaultdict + +class UniIndex: + def __init__(self, index_properties): + self._index_properties = index_properties + self._index_map = defaultdict(list) + + def put(self, tuple_): + key = self._index_properties.get_property(tuple_) + self._index_map[key].append(tuple_) + + def get(self, key): + return self._index_map.get(key, []) + + def remove(self, tuple_): + key = self._index_properties.get_property(tuple_) + if key in self._index_map: + try: + self._index_map[key].remove(tuple_) + if not self._index_map[key]: + del self._index_map[key] + except ValueError: + pass # Tuple was already removed. \ No newline at end of file diff --git a/greyjack/greyjack/score_calculation/greynet/common/index_properties.py b/greyjack/greyjack/score_calculation/greynet/common/index_properties.py new file mode 100644 index 0000000..808a740 --- /dev/null +++ b/greyjack/greyjack/score_calculation/greynet/common/index_properties.py @@ -0,0 +1,7 @@ +# greynet/common/index_properties.py +class IndexProperties: + def __init__(self, property_retriever): + self._property_retriever = property_retriever + + def get_property(self, obj): + return self._property_retriever(obj) \ No newline at end of file diff --git a/greyjack/greyjack/score_calculation/greynet/common/joiner_type.py b/greyjack/greyjack/score_calculation/greynet/common/joiner_type.py new file mode 100644 index 0000000..4f810bf --- /dev/null +++ b/greyjack/greyjack/score_calculation/greynet/common/joiner_type.py @@ -0,0 +1,48 @@ +# greynet/common/joiner_type.py +from enum import Enum + +class JoinerType(Enum): + EQUAL = "equal" + LESS_THAN = "less_than" + LESS_THAN_OR_EQUAL = "less_than_or_equal" + GREATER_THAN = "greater_than" + GREATER_THAN_OR_EQUAL = "greater_than_or_equal" + NOT_EQUAL = "not_equal" + RANGE_OVERLAPS = "range_overlaps" + RANGE_CONTAINS = "range_contains" + RANGE_WITHIN = "range_within" + + def create_comparator(self): + comparators = { + JoinerType.EQUAL: lambda a, b: a == b, + JoinerType.LESS_THAN: lambda a, b: a < b, + JoinerType.LESS_THAN_OR_EQUAL: lambda a, b: a <= b, + JoinerType.GREATER_THAN: lambda a, b: a > b, + JoinerType.GREATER_THAN_OR_EQUAL: lambda a, b: a >= b, + JoinerType.NOT_EQUAL: lambda a, b: a != b, + JoinerType.RANGE_OVERLAPS: self._range_overlaps, + JoinerType.RANGE_CONTAINS: self._range_contains, + JoinerType.RANGE_WITHIN: self._range_within, + } + return comparators[self] + + @staticmethod + def _validate_range(val, name="range"): + if not isinstance(val, (list, tuple)) or len(val) != 2: + raise TypeError(f"{name} must be a tuple or list of length 2, representing [start, end]") + + @staticmethod + def _range_overlaps(range_a, range_b): + JoinerType._validate_range(range_a, "range_a") + JoinerType._validate_range(range_b, "range_b") + return not (range_a[1] < range_b[0] or range_b[1] < range_a[0]) + + @staticmethod + def _range_contains(container_range, content_range): + JoinerType._validate_range(container_range, "container_range") + JoinerType._validate_range(content_range, "content_range") + return container_range[0] <= content_range[0] and content_range[1] <= container_range[1] + + @staticmethod + def _range_within(content_range, container_range): + return JoinerType._range_contains(container_range, content_range) \ No newline at end of file diff --git a/greyjack/greyjack/score_calculation/greynet/constraint.py b/greyjack/greyjack/score_calculation/greynet/constraint.py new file mode 100644 index 0000000..b26aa33 --- /dev/null +++ b/greyjack/greyjack/score_calculation/greynet/constraint.py @@ -0,0 +1,20 @@ +# greynet/constraint.py +from __future__ import annotations +from typing import Callable, TYPE_CHECKING + +if TYPE_CHECKING: + from .streams.abstract_stream import AbstractStream + +class Constraint: + """ + A data-only class that holds the definition of a constraint before it is + fully processed by the ConstraintBuilder. + + By moving this class to its own file, we break the circular dependency between + the 'builder' and 'streams' modules. + """ + def __init__(self, stream: 'AbstractStream', score_type: str, penalty_function: Callable, constraint_id: str = None): + self.stream = stream + self.constraint_id = constraint_id + self.score_type = score_type + self.penalty_function = penalty_function diff --git a/greyjack/greyjack/score_calculation/greynet/constraint_factory.py b/greyjack/greyjack/score_calculation/greynet/constraint_factory.py new file mode 100644 index 0000000..9e7d1b8 --- /dev/null +++ b/greyjack/greyjack/score_calculation/greynet/constraint_factory.py @@ -0,0 +1,56 @@ +from __future__ import annotations +from typing import Type + +from .nodes.from_uni_node import FromUniNode +from .nodes.scoring_node import ScoringNode +from .session import Session +from .optimization.batch_processor import BatchScheduler +from .core.tuple_pool import TuplePool +from .optimization.node_sharing import NodeSharingManager +from .streams.stream import Stream +from .streams.stream_definition import FromDefinition +from .core.tuple import UniTuple + +class ConstraintFactory: + def __init__(self, package_name: str, score_class: Type): + self.package_name = package_name + self.score_class = score_class + self._constraint_defs = [] + self.tuple_pool = TuplePool() + self.node_sharer = NodeSharingManager() + + def from_(self, from_class) -> Stream[UniTuple]: + """Creates a new Stream originating from a fact class.""" + from_def = FromDefinition(self, from_class) + return Stream[UniTuple](self, from_def) + + + def add_constraint(self, constraint_def): + self._constraint_defs.append(constraint_def) + + def build_session(self, **kwargs) -> Session: + class Counter: + def __init__(self): self.value = 0 + node_counter = Counter() + + weights = kwargs.pop('weights', None) + + session_node_map = {} + scheduler = BatchScheduler( + session_node_map, + self.tuple_pool, + kwargs.get("batch_size", 100) + ) + + for constraint_def in self._constraint_defs: + final_stream = constraint_def() + final_stream.build_node(node_counter, session_node_map, scheduler, self.tuple_pool) + + from_nodes, scoring_nodes = {}, [] + for node in session_node_map.values(): + if isinstance(node, FromUniNode): + from_nodes[node.retrieval_id] = node + elif isinstance(node, ScoringNode): + scoring_nodes.append(node) + + return Session(from_nodes, scoring_nodes, scheduler, self.score_class, self.tuple_pool, weights) diff --git a/greyjack/greyjack/score_calculation/greynet/constraint_tools/__init__.py b/greyjack/greyjack/score_calculation/greynet/constraint_tools/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/greyjack/greyjack/score_calculation/greynet/constraint_tools/connected_range_tracker.py b/greyjack/greyjack/score_calculation/greynet/constraint_tools/connected_range_tracker.py new file mode 100644 index 0000000..ece81fc --- /dev/null +++ b/greyjack/greyjack/score_calculation/greynet/constraint_tools/connected_range_tracker.py @@ -0,0 +1,105 @@ + +# greynet/constraint_tools/connected_range_tracker.py + +from dataclasses import dataclass +from typing import Any, List + +@dataclass(order=True) +class ConnectedRange: + start: Any + end: Any + data: List[Any] + + def can_connect(self, other): + # Ranges connect if they overlap or are immediately adjacent. + return not (self.end < other.start or other.end < self.start) + + def merge(self, other): + return ConnectedRange( + start=min(self.start, other.start), + end=max(self.end, other.end), + data=self.data + other.data + ) + +class ConnectedRangeTracker: + def __init__(self, start_mapping, end_mapping): + self._start_mapping = start_mapping + self._end_mapping = end_mapping + self._ranges = [] + self._item_to_range = {} + + def add(self, item): + start, end = self._start_mapping(item), self._end_mapping(item) + new_range = ConnectedRange(start=start, end=end, data=[item]) + + overlapping, remaining = [], [] + for r in self._ranges: + if new_range.can_connect(r): + overlapping.append(r) + else: + remaining.append(r) + + merged = new_range + for r in overlapping: + merged = merged.merge(r) + + self._ranges = remaining # Temporarily remove merged ranges + self._insert_sorted(merged) # Add the new merged range + + # Update the item-to-range mapping for all items in the new range + for item_in_merged in merged.data: + self._item_to_range[item_in_merged] = merged + + def remove(self, item): + if item not in self._item_to_range: + return + + containing_range = self._item_to_range.pop(item) + self._ranges.remove(containing_range) + + remaining_items = [i for i in containing_range.data if i != item] + if not remaining_items: + return + + # Rebuild ranges from the remaining items, as the removal might + # have split a single continuous range into two or more. + new_ranges = self._rebuild_ranges(remaining_items) + for r in new_ranges: + self._insert_sorted(r) + for i in r.data: + self._item_to_range[i] = r + + def get_connected_ranges(self): + return self._ranges.copy() + + def _insert_sorted(self, range_): + import bisect + bisect.insort_left(self._ranges, range_, key=lambda r: r.start) + + def _rebuild_ranges(self, items): + if not items: + return [] + # Sort items by their start time to reconstruct ranges efficiently + sorted_items = sorted(items, key=self._start_mapping) + + rebuilt_ranges = [] + current_range = ConnectedRange( + self._start_mapping(sorted_items[0]), + self._end_mapping(sorted_items[0]), + [sorted_items[0]] + ) + + for item in sorted_items[1:]: + item_range = ConnectedRange( + self._start_mapping(item), + self._end_mapping(item), + [item] + ) + if current_range.can_connect(item_range): + current_range = current_range.merge(item_range) + else: + rebuilt_ranges.append(current_range) + current_range = item_range + + rebuilt_ranges.append(current_range) + return rebuilt_ranges diff --git a/greyjack/greyjack/score_calculation/greynet/constraint_tools/consecutive_set_tree.py b/greyjack/greyjack/score_calculation/greynet/constraint_tools/consecutive_set_tree.py new file mode 100644 index 0000000..f019fc9 --- /dev/null +++ b/greyjack/greyjack/score_calculation/greynet/constraint_tools/consecutive_set_tree.py @@ -0,0 +1,134 @@ + +# greynet/constraint_tools/consecutive_set_tree.py + +from dataclasses import dataclass +from typing import Any, Tuple + +@dataclass(frozen=True, order=True) +class ConsecutiveSequence: + """Represents a continuous sequence of items.""" + start: Any + end: Any + length: int + items: Tuple[Any, ...] + +class ConsecutiveSetTree: + """ + A data structure for efficiently tracking consecutive sequences of items. + """ + def __init__(self, sequence_function, increment_function): + self._sequence_func = sequence_function + self._inc_func = increment_function + self._item_to_pos = {} + self._pos_to_seq = {} + self._sequences = set() + + def add(self, item): + """Adds an item and updates the sequences accordingly.""" + if item in self._item_to_pos: + return + + pos = self._sequence_func(item) + self._item_to_pos[item] = pos + + prev_pos = self._inc_func(pos, -1) + next_pos = self._inc_func(pos, 1) + + prev_seq = self._pos_to_seq.get(prev_pos) + next_seq = self._pos_to_seq.get(next_pos) + + if prev_seq and next_seq: + if prev_seq is not next_seq: + self._merge(prev_seq, next_seq, item, pos) + elif prev_seq: + self._extend(prev_seq, item, pos) + elif next_seq: + self._prepend(next_seq, item, pos) + else: + self._create(item, pos) + + def remove(self, item): + """ + Robustly removes an item by finding its sequence, tearing it down, + and rebuilding new sequences from the remaining items. This approach + correctly handles cases where a sequence is split in two. + """ + if item not in self._item_to_pos: + return + + pos = self._item_to_pos[item] # Get position without popping + seq = self._pos_to_seq.get(pos) + + if not seq: + # The item was mapped but not part of a sequence. Just clean up its own mappings. + del self._item_to_pos[item] + if self._pos_to_seq.get(pos) is None: + del self._pos_to_seq[pos] + return + + # 1. Identify which items we need to re-add later. + items_to_re_add = [i for i in seq.items if i is not item] + + # 2. Completely remove the old sequence and all its associated mappings + # to ensure a clean state before rebuilding. + self._sequences.discard(seq) + for i in seq.items: + if i in self._item_to_pos: + item_pos = self._item_to_pos.pop(i) + if self._pos_to_seq.get(item_pos) is seq: + del self._pos_to_seq[item_pos] + + # Clean boundary pointers as a safeguard. + if self._pos_to_seq.get(seq.start) is seq: + del self._pos_to_seq[seq.start] + if self._pos_to_seq.get(seq.end) is seq: + del self._pos_to_seq[seq.end] + + # 3. Re-add the remaining items. The `add` method will correctly form + # new sequences (e.g., splitting the old one into two). + for i in items_to_re_add: + self.add(i) + + def _update_mappings(self, seq): + """Updates internal dictionaries to map positions to the new sequence.""" + for item in seq.items: + pos = self._item_to_pos.get(item) + if pos is not None: + self._pos_to_seq[pos] = seq + self._pos_to_seq[seq.start] = seq + self._pos_to_seq[seq.end] = seq + + def _create(self, item, pos): + """Creates a new sequence of length 1.""" + seq = ConsecutiveSequence(start=pos, end=pos, length=1, items=(item,)) + self._sequences.add(seq) + self._update_mappings(seq) + + def _extend(self, seq, item, pos): + """Adds an item to the end of an existing sequence.""" + self._sequences.remove(seq) + new_items = seq.items + (item,) + new_seq = ConsecutiveSequence(start=seq.start, end=pos, length=len(new_items), items=new_items) + self._sequences.add(new_seq) + self._update_mappings(new_seq) + + def _prepend(self, seq, item, pos): + """Adds an item to the beginning of an existing sequence.""" + self._sequences.remove(seq) + new_items = (item,) + seq.items + new_seq = ConsecutiveSequence(start=pos, end=seq.end, length=len(new_items), items=new_items) + self._sequences.add(new_seq) + self._update_mappings(new_seq) + + def _merge(self, prev_seq, next_seq, item, pos): + """Merges two sequences with a new item in between.""" + self._sequences.remove(prev_seq) + self._sequences.remove(next_seq) + new_items = prev_seq.items + (item,) + next_seq.items + new_seq = ConsecutiveSequence(start=prev_seq.start, end=next_seq.end, length=len(new_items), items=new_items) + self._sequences.add(new_seq) + self._update_mappings(new_seq) + + def get_sequences(self): + """Returns the current list of all consecutive sequences.""" + return list(self._sequences) diff --git a/greyjack/greyjack/score_calculation/greynet/constraint_tools/counting_bloom_filter.py b/greyjack/greyjack/score_calculation/greynet/constraint_tools/counting_bloom_filter.py new file mode 100644 index 0000000..c942873 --- /dev/null +++ b/greyjack/greyjack/score_calculation/greynet/constraint_tools/counting_bloom_filter.py @@ -0,0 +1,124 @@ +# greynet/constraint_tools/counting_bloom_filter.py +import mmh3 +from bitarray import bitarray +import math + +class CountingBloomFilter: + """ + A Counting Bloom Filter that supports adding, removing, and checking for items. + + This is essential for the rules engine where facts can be inserted and retracted. + Instead of single bits, it uses a bit array to store n-bit counters. + """ + + def __init__(self, estimated_items: int, false_positive_rate: float, cell_bits: int = 4): + """ + Initializes the Counting Bloom Filter. + + Args: + estimated_items (int): The expected number of items to be stored. + false_positive_rate (float): The desired false positive probability. + cell_bits (int): The number of bits per counter cell (e.g., 4, 8). + This determines the max count before overflow. + """ + if not (0 < false_positive_rate < 1): + raise ValueError("False positive rate must be between 0 and 1.") + if estimated_items <= 0: + raise ValueError("Estimated items must be positive.") + + # Calculate optimal size and number of hashes + self.size = self._get_size(estimated_items, false_positive_rate) + self.num_hashes = self._get_hash_count(self.size, estimated_items) + self.cell_bits = cell_bits + self.max_count = (1 << cell_bits) - 1 + + # The bit array size is the number of cells * bits per cell + self.bit_array = bitarray(self.size * self.cell_bits) + self.bit_array.setall(0) + + self.item_count = 0 + + @staticmethod + def _get_size(n, p): + """Calculate bit array size (m) from items (n) and false positive rate (p).""" + m = - (n * math.log(p)) / (math.log(2) ** 2) + return int(math.ceil(m)) + + @staticmethod + def _get_hash_count(m, n): + """Calculate optimal number of hash functions (k) from size (m) and items (n).""" + k = (m / n) * math.log(2) + return int(math.ceil(k)) + + def _get_hashes(self, item_str: str) -> list[int]: + """Generate num_hashes hash values for an item.""" + # Use two hashes to generate k hashes, a common and fast technique + # FIX: The seed for the second hash must be an unsigned 32-bit integer. + # By setting signed=False, we ensure h1 is always in the valid range. + h1 = mmh3.hash(item_str, seed=0, signed=False) + h2 = mmh3.hash(item_str, seed=h1) + return [(h1 + i * h2) % self.size for i in range(self.num_hashes)] + + def _get_cell_value(self, index: int) -> int: + """Reads the integer value from a counter cell.""" + start = index * self.cell_bits + end = start + self.cell_bits + return int.from_bytes(self.bit_array[start:end].tobytes(), 'little') + + def _set_cell_value(self, index: int, value: int): + """Writes an integer value to a counter cell.""" + start = index * self.cell_bits + end = start + self.cell_bits + val_bits = bitarray() + val_bits.frombytes(value.to_bytes(math.ceil(self.cell_bits / 8), 'little')) + self.bit_array[start:end] = val_bits[:self.cell_bits] + + def add(self, item: any): + """ + Adds an item to the filter by incrementing its corresponding counters. + """ + item_str = str(item) + hashes = self._get_hashes(item_str) + + # Check if all cells can be incremented before doing so + for index in hashes: + if self._get_cell_value(index) >= self.max_count: + raise OverflowError(f"Counter for item '{item}' would overflow.") + + for index in hashes: + current_value = self._get_cell_value(index) + self._set_cell_value(index, current_value + 1) + self.item_count += 1 + + def remove(self, item: any): + """ + Removes an item by decrementing its corresponding counters. + Does nothing if the item was likely not present. + """ + if item not in self: + return + + item_str = str(item) + hashes = self._get_hashes(item_str) + for index in hashes: + current_value = self._get_cell_value(index) + if current_value > 0: + self._set_cell_value(index, current_value - 1) + self.item_count -= 1 + + def __contains__(self, item: any) -> bool: + """ + Checks if an item is in the filter. + Returns False if the item is definitely not in the set. + Returns True if the item is probably in the set (with a chance of being a false positive). + """ + item_str = str(item) + hashes = self._get_hashes(item_str) + for index in hashes: + if self._get_cell_value(index) == 0: + return False + return True + + def __len__(self) -> int: + """Returns the number of items added to the filter.""" + return self.item_count diff --git a/greyjack/greyjack/score_calculation/greynet/constraint_weights.py b/greyjack/greyjack/score_calculation/greynet/constraint_weights.py new file mode 100644 index 0000000..f596f68 --- /dev/null +++ b/greyjack/greyjack/score_calculation/greynet/constraint_weights.py @@ -0,0 +1,29 @@ +# greynet/constraint_weights.py +import threading +from typing import Dict + +class ConstraintWeights: + """ + A thread-safe manager for storing and retrieving constraint weights. + + This object is shared between the builder and the session to allow + for dynamic, runtime updates to constraint penalties. + """ + def __init__(self): + self._weights: Dict[str, float] = {} + self._lock = threading.Lock() + + def set_weight(self, constraint_id: str, weight: float): + """Sets the weight multiplier for a given constraint.""" + with self._lock: + self._weights[constraint_id] = float(weight) + + def get_weight(self, constraint_id: str) -> float: + """ + Gets the weight for a given constraint. + + Returns: + The configured weight, or 1.0 as a default if not set. + """ + with self._lock: + return self._weights.get(constraint_id, 1.0) diff --git a/greyjack/greyjack/score_calculation/greynet/core/__init__.py b/greyjack/greyjack/score_calculation/greynet/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/greyjack/greyjack/score_calculation/greynet/core/scheduler.py b/greyjack/greyjack/score_calculation/greynet/core/scheduler.py new file mode 100644 index 0000000..d26e8ec --- /dev/null +++ b/greyjack/greyjack/score_calculation/greynet/core/scheduler.py @@ -0,0 +1,14 @@ +# greynet/core/scheduler.py +from abc import ABC, abstractmethod + +class Scheduler(ABC): + def __init__(self, node_map): + self._node_map = node_map + + @abstractmethod + def schedule(self, tuple_): + pass + + @abstractmethod + def fire_all(self): + pass \ No newline at end of file diff --git a/greyjack/greyjack/score_calculation/greynet/core/tuple.py b/greyjack/greyjack/score_calculation/greynet/core/tuple.py new file mode 100644 index 0000000..9cd07db --- /dev/null +++ b/greyjack/greyjack/score_calculation/greynet/core/tuple.py @@ -0,0 +1,103 @@ +# core/tuple.py +from enum import Enum +from dataclasses import dataclass +from typing import Any + +class TupleState(Enum): + CREATING = "CREATING" + OK = "OK" + UPDATING = "UPDATING" + DYING = "DYING" + ABORTING = "ABORTING" + DEAD = "DEAD" + + def is_dirty(self): + return self in {TupleState.CREATING, TupleState.UPDATING, TupleState.DYING} + +@dataclass(eq=False) +class AbstractTuple: + node: Any = None + state: Any = None + + def __eq__(self, other): + return self is other + + def __hash__(self): + return id(self) + + def reset(self): + """Resets the base attributes of the tuple for pooling.""" + self.node = None + self.state = None + + + + +@dataclass(eq=False) +class UniTuple(AbstractTuple): + fact_a: Any = None + + def reset(self): + """Resets the tuple for pooling.""" + super().reset() + self.fact_a = None + + +@dataclass(eq=False) +class BiTuple(AbstractTuple): + fact_a: Any = None + fact_b: Any = None + + def reset(self): + """Resets the tuple for pooling.""" + super().reset() + self.fact_a = None + self.fact_b = None + + +@dataclass(eq=False) +class TriTuple(AbstractTuple): + fact_a: Any = None + fact_b: Any = None + fact_c: Any = None + + def reset(self): + """Resets the tuple for pooling.""" + super().reset() + self.fact_a = None + self.fact_b = None + self.fact_c = None + + +@dataclass(eq=False) +class QuadTuple(AbstractTuple): + fact_a: Any = None + fact_b: Any = None + fact_c: Any = None + fact_d: Any = None + + def reset(self): + """Resets the tuple for pooling.""" + super().reset() + self.fact_a = None + self.fact_b = None + self.fact_c = None + self.fact_d = None + + +@dataclass(eq=False) +class PentaTuple(AbstractTuple): + fact_a: Any = None + fact_b: Any = None + fact_c: Any = None + fact_d: Any = None + fact_e: Any = None + + def reset(self): + """Resets the tuple for pooling.""" + super().reset() + self.fact_a = None + self.fact_b = None + self.fact_c = None + self.fact_d = None + self.fact_e = None diff --git a/greyjack/greyjack/score_calculation/greynet/core/tuple_pool.py b/greyjack/greyjack/score_calculation/greynet/core/tuple_pool.py new file mode 100644 index 0000000..15da2bc --- /dev/null +++ b/greyjack/greyjack/score_calculation/greynet/core/tuple_pool.py @@ -0,0 +1,66 @@ +# greynet/core/tuple_pool.py +from __future__ import annotations +from collections import defaultdict, deque +from typing import Type, TypeVar + +from ..core.tuple import AbstractTuple + +T = TypeVar('T', bound=AbstractTuple) + + +class TuplePool: + """ + Manages pools of tuple objects to reduce memory allocation overhead. + + This class maintains a separate pool (a deque) for each tuple type. + When a tuple is requested, it tries to retrieve one from the pool. + If the pool is empty, a new tuple is created. When a tuple is + no longer needed, it's released back to the pool for reuse. + """ + def __init__(self): + self._pools: defaultdict[Type[T], deque[T]] = defaultdict(deque) + + def acquire(self, tuple_class: Type[T], **kwargs) -> T: + """ + Acquires a tuple instance, either by reusing one from the pool or + creating a new one. + + Args: + tuple_class: The class of the tuple to acquire (e.g., UniTuple). + **kwargs: The initial attributes for the tuple (e.g., fact_a). + + Returns: + An initialized instance of the requested tuple class. + """ + pool = self._pools[tuple_class] + if pool: + # Reuse an existing tuple from the pool + tuple_instance = pool.popleft() + # Re-initialize its attributes + for key, value in kwargs.items(): + setattr(tuple_instance, key, value) + return tuple_instance + else: + # Create a new tuple if the pool is empty + return tuple_class(**kwargs) + + def release(self, tuple_instance: T): + """ + Releases a tuple back to the pool for future reuse. + + The tuple is reset to a clean state before being added back. + + Args: + tuple_instance: The tuple object to release. + """ + tuple_class = type(tuple_instance) + # Ensure the tuple is cleaned before pooling + tuple_instance.reset() + self._pools[tuple_class].append(tuple_instance) + + def stats(self) -> dict[str, int]: + """Returns the current size of each pool for diagnostics.""" + return { + cls.__name__: len(pool) + for cls, pool in self._pools.items() + } diff --git a/greyjack/greyjack/score_calculation/greynet/docs/manual_1.md b/greyjack/greyjack/score_calculation/greynet/docs/manual_1.md new file mode 100644 index 0000000..6fcdc23 --- /dev/null +++ b/greyjack/greyjack/score_calculation/greynet/docs/manual_1.md @@ -0,0 +1,628 @@ +Of course. Here is a comprehensive, top-down guide to the Greynet library, crafted from the provided source code. + +## Greynet: The Definitive Manual + +Welcome to the comprehensive guide for Greynet, a powerful, forward-chaining rule engine for Python. Greynet is designed for complex event processing (CEP) and sophisticated constraint satisfaction problems, enabling you to define declarative rules that operate on streams of data in real-time. + +This manual provides a top-down look at the library, from high-level concepts to the intricacies of its powerful API. + +### 1. High-Level Overview + +#### What is Greynet? + +Greynet is a declarative rule engine inspired by the high-performance Rete algorithm. It allows you to define a set of **constraints** (rules) over a collection of data **facts**. When you insert, update, or retract facts, Greynet efficiently re-evaluates only the affected rules, calculates a **score** based on any violations, and provides a detailed breakdown of which facts matched which constraints. + +Its core strengths are: + +* **Declarative, Fluent API:** Define complex logic through a clean, chainable stream-processing API. +* **High Performance:** Under the hood, Greynet builds an optimized network of nodes, sharing common logic between rules to avoid redundant calculations. +* **Rich Feature Set:** Supports advanced operations including complex joins, aggregations, conditional logic (`if_exists`/`if_not_exists`), and powerful temporal pattern matching. +* **Dynamic and Incremental:** The engine reacts incrementally to data changes, making it suitable for real-time applications. + +#### Core Concepts + +Greynet's architecture can be visualized as a data processing pipeline. + +```mermaid +graph TD + A[Facts: Plain Python Objects] --> B(ConstraintBuilder); + B --> C{Stream API: filter, join, group_by}; + C --> D[Constraint Definition: .penalize(...)]; + B -- build() --> E(Session); + A -- insert()/retract() --> E; + E -- Manages --> F((Rete Network)); + F -- Produces --> G[Score & Matches]; +``` + +| Concept | Description | +|:----------------------- |:------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Fact** | A plain Python object (often a `dataclass`) representing a piece of data in the system, like a `User` or an `Appointment`. | +| **`ConstraintBuilder`** | The main factory class used to define all constraints and build the final `Session`. | +| **`Stream`** | A fluent, chainable object that represents a flow of data. You start with a stream of facts and apply operations like `filter()`, `join()`, and `group_by()`. | +| **`Constraint`** | A rule defined by a decorated function. It is composed of a `Stream` that ends with a `.penalize()` call. | +| **`Session`** | The runtime engine. You insert facts into the session, and it calculates the total score and provides a list of all constraint matches. | +| **Collectors** | Powerful aggregation tools used with `group_by()` to calculate counts, sums, averages, lists, sets, and more. | + +--- + +### 2. Getting Started: A Simple Example + +Let's define a simple rule: "No user should have an 'admin' role". + +#### Step 1: Define the Fact + +A fact is just a simple Python object. A `dataclass` is often a good choice. + +```python +from dataclasses import dataclass + +@dataclass +class User: + id: int + name: str + role: str +``` + +#### Step 2: Define the Constraint + +Use the `ConstraintBuilder` and the `@builder.constraint` decorator to define your rule. + +```python +from greynet import ConstraintBuilder + +# 1. Initialize the builder +builder = ConstraintBuilder() + +# 2. Define a constraint using the decorator +@builder.constraint("no_admin_users", default_weight=1.0) +def no_admins(): + # 3. Start a stream of facts from the User class + return (builder.for_each(User) + # 4. Filter for users where the role is 'admin' + .filter(lambda user: user.role == 'admin') + # 5. Penalize each match with a score of 100 + .penalize_hard(100) + ) +``` + +#### Step 3: Build and Use the Session + +Build the session from the builder, insert facts, and check the score. + +```python +# 1. Build the session +session = builder.build() + +# 2. Create some facts +user1 = User(id=1, name="Alice", role="editor") +user2 = User(id=2, name="Bob", role="admin") +user3 = User(id=3, name="Charlie", role="admin") + +# 3. Insert facts into the session +session.insert_batch([user1, user2, user3]) + +# 4. Get the total score +total_score = session.get_score() +print(f"Total Score: {total_score.hard_score}") # Output: Total Score: 200.0 + +# 5. Get a detailed breakdown of matches +matches = session.get_constraint_matches() +# `matches` will be a dictionary like: +# { +# 'no_admin_users': [ +# (score_object, (User(id=2, ...),)), +# (score_object, (User(id=3, ...),)) +# ] +# } +print(f"Found {len(matches.get('no_admin_users', []))} violations.") # Output: Found 2 violations. + +# 6. Retract a fact and see the score update +session.retract(user2) +total_score_after_retract = session.get_score() +print(f"New Total Score: {total_score_after_retract.hard_score}") # Output: New Total Score: 100.0 +``` + +--- + +### 3. The Fluent Stream API + +The `Stream` object is the core of rule definition. All operations are chainable, allowing you to compose complex logic fluently. + +A stream processes tuples of facts. A stream from `for_each` contains `UniTuple` (one fact). After a join, it might contain `BiTuple` (two facts), and so on. + +#### Creating Streams + +* **`builder.for_each(FactClass)`** + This is the primary way to start a stream. It listens for all insertions of the specified `FactClass`. + +#### Filtering + +* **`.filter(lambda *facts: ...)`** + Filters the stream, only allowing tuples that satisfy the predicate to pass through. The lambda receives the facts contained in the stream's tuple. + + ```python + # UniTuple stream (1 fact) + builder.for_each(Appointment).filter(lambda appt: appt.is_confirmed) + + # BiTuple stream (2 facts) from a join + stream.filter(lambda user, appt: user.id == appt.user_id) + ``` + +#### Joining + +* **`.join(other_stream, joiner_type, left_key_func, right_key_func)`** + Combines two streams into one. The resulting stream contains tuples with facts from both parents. + **`JoinerType`** + This enum specifies the join condition. + + | JoinerType | Description | + |:------------------------ |:----------------------------------- | + | `EQUAL` | Keys are equal (most common). | + | `NOT_EQUAL` | Keys are not equal. | + | `LESS_THAN` | Left key is less than right key. | + | `GREATER_THAN` | Left key is greater than right key. | + | `LESS_THAN_OR_EQUAL` | ... | + | `GREATER_THAN_OR_EEQUAL` | ... | + + **Example:** Find users who have scheduled appointments. + + ```python + users = builder.for_each(User) + appointments = builder.for_each(Appointment) + + users_with_appts = users.join(appointments, + JoinerType.EQUAL, + left_key_func=lambda user: user.id, + right_key_func=lambda appt: appt.user_id + ) + # The 'users_with_appts' stream now contains (user, appointment) pairs. + ``` + +#### Conditional Existence + +These are powerful tools for expressing rules based on the presence or absence of related data, without adding that data to the stream. + +* **`.if_exists(other_stream, ...)`** + Acts as a filter. A tuple from the main stream is propagated only if a matching tuple exists in the `other_stream`. + **Example:** Find users who have at least one invoice. + + ```python + users = builder.for_each(User) + invoices = builder.for_each(Invoice) + + users_with_invoices = users.if_exists(invoices, + left_key=lambda user: user.id, + right_key=lambda invoice: invoice.customer_id + ) + # The 'users_with_invoices' stream still contains only User facts. + ``` + +* **`.if_not_exists(other_stream, ...)`** + The opposite of `if_exists`. A tuple is propagated only if **no** matching tuple exists in the `other_stream`. + **Example:** Find active users who have no overdue tasks. + + ```python + active_users = builder.for_each(User).filter(lambda u: u.is_active) + overdue_tasks = builder.for_each(Task).filter(lambda t: t.is_overdue) + + users_with_no_overdue_tasks = active_users.if_not_exists(overdue_tasks, + left_key=lambda user: user.id, + right_key=lambda task: task.assignee_id + ) + ``` + +#### Aggregation + +* **`.group_by(group_key_function, collector)`** + This is one of Greynet's most powerful features. It groups facts from a stream by a key and applies a `Collector` to each group to produce an aggregate result. + The output is a new stream of `BiTuple`s containing `(group_key, aggregate_result)`. + **Example:** Count the number of tasks for each user. + + ```python + from greynet import Collectors + + tasks = builder.for_each(Task) + + tasks_per_user = tasks.group_by( + group_key_function=lambda task: task.assignee_id, + collector_supplier=Collectors.count() + ) + # The 'tasks_per_user' stream contains (user_id, count) pairs. + ``` + +#### Transformation + +* **`.map(lambda *facts: ...)`** + Transforms each tuple in the stream into a new single-item `UniTuple`. This is a 1-to-1 transformation. + +* **`.flat_map(lambda *facts: ...)`** + Transforms each tuple into an iterable of new items. Each item from the iterable becomes a new `UniTuple` in the output stream. This is a 1-to-many transformation. + **Example:** From a stream of `Order` facts, create a new stream of `OrderItem` facts. + + ```python + orders = builder.for_each(Order) + + order_items = orders.flat_map(lambda order: order.items) + # 'order_items' is now a stream of individual OrderItem facts. + ``` + +--- + +### 4. The Collectors API + +The `greynet.Collectors` namespace provides a rich set of tools for aggregation within a `group_by` operation. + +#### Basic Aggregations + +These collectors perform standard mathematical aggregations. Most require a mapping function to extract a numeric value from the fact. + +* `Collectors.count()`: Counts items in a group. +* `Collectors.sum(lambda fact: fact.amount)`: Sums values. +* `Collectors.avg(lambda fact: fact.score)`: Calculates the average. +* `Collectors.min(lambda fact: fact.price)`: Finds the minimum value. +* `Collectors.max(lambda fact: fact.price)`: Finds the maximum value. +* `Collectors.stddev(lambda fact: fact.value)`: Calculates standard deviation. +* `Collectors.variance(lambda fact: fact.value)`: Calculates variance. + +#### Collection Aggregations + +These collectors gather facts into collections. + +* `Collectors.to_list()`: Collects all facts in a group into a list. +* `Collectors.to_set()`: Collects unique facts into a set. +* `Collectors.distinct()`: Collects unique items into a list, preserving insertion order. + +#### Advanced and Composite Collectors + +* **`Collectors.compose({...})`** + Performs multiple aggregations on the same group in a single pass for maximum efficiency. The result is a dictionary. + + ```python + from greynet import Collectors + + stats_per_product = builder.for_each(Sale).group_by( + lambda sale: sale.product_id, + Collectors.compose({ + 'sales_count': Collectors.count(), + 'total_revenue': Collectors.sum(lambda s: s.price), + 'avg_price': Collectors.avg(lambda s: s.price) + }) + ) + # Output stream contains: + # (product_id, {'sales_count': 10, 'total_revenue': 550.0, ...}) + ``` + +* **`Collectors.filtering(predicate, downstream_collector)`** + Filters items *within* a group before passing them to another collector. + +* **`Collectors.mapping(mapper, downstream_collector)`** + Maps items *within* a group before passing them to another collector. + +#### Specialized Collectors + +Greynet provides highly specialized collectors for complex pattern detection within groups. + +* **`Collectors.consecutive_sequences(sequence_func, ...)`** + Identifies runs of consecutive items. For example, finding consecutive login days for a user. +* **`Collectors.connected_ranges(start_func, end_func)`** + Merges items that represent overlapping or adjacent ranges into single continuous ranges. Useful for scheduling problems to find blocks of busy/free time. +* **`Collectors.to_bloom_filter(...)`** + Aggregates items into a `CountingBloomFilter` for efficient, probabilistic set membership tests. + +--- + +### 5. Temporal and Sequential Patterns + +Greynet includes first-class support for time-based rules, crucial for CEP scenarios. + +#### Windowing + +Windowing operations group facts based on their timestamps. You initiate windowing with `.window(time_extractor)` followed by a window type. The `time_extractor` is a function that returns a `datetime` object from your fact. + +* **`.window(...).tumbling(size=timedelta)`** + Creates fixed-size, non-overlapping windows. An event belongs to exactly one window. +* **`.window(...).sliding(size=timedelta, slide=timedelta)`** + Creates fixed-size, overlapping windows. An event can belong to multiple windows. + +**Example:** Count the number of logins every hour, updated every 5 minutes. + +```python +from datetime import timedelta +from greynet import Collectors + +logins_per_window = (builder.for_each(LoginEvent) + .window(lambda event: event.timestamp) + .sliding(size=timedelta(hours=1), slide=timedelta(minutes=5)) + .group_by( + lambda window_start, events_in_window: window_start, + Collectors.mapping( + lambda ws, elist: len(elist), # Map the tuple to just the count + Collectors.sum(lambda count: count) # Sum the counts + ) + ) +) +``` + +#### Sequence Detection + +* **`.sequence(time_extractor, *steps, within=timedelta, allow_gaps=bool)`** + This powerful feature detects when facts occur in a specific order within a time limit. It takes a series of predicates (`*steps`) that must be satisfied sequentially. + +**Example:** Detect when a user adds an item to their cart, then views the checkout page, but does not complete the purchase within 10 minutes. + +```python +from datetime import timedelta + +# Define predicates for each step in the sequence +def is_add_to_cart(event): return event.type == 'ADD_TO_CART' +def is_view_checkout(event): return event.type == 'VIEW_CHECKOUT' + +# Stream of sequences: (add_to_cart_event, view_checkout_event) +potential_abandonment = (builder.for_each(UserEvent) + .sequence( + lambda event: event.timestamp, + is_add_to_cart, + is_view_checkout, + within=timedelta(minutes=10) + ) +) + +# Stream of completed purchases +purchases = builder.for_each(UserEvent).filter(lambda e: e.type == 'PURCHASE') + +# Final rule: penalize if the sequence occurs and a purchase does NOT. +abandoned_carts = potential_abandonment.if_not_exists(purchases, + # Join on user ID + left_key=lambda sequence: sequence[0].user_id, + right_key=lambda purchase: purchase.user_id +) +``` + +--- + +### 6. Scoring, Penalties, and Weights + +Every constraint must end with a `.penalize()` call, which defines the score impact when the rule is matched. + +#### Penalty Methods + +* `.penalize_hard(penalty)` +* `.penalize_medium(penalty)` +* `.penalize_soft(penalty)` +* `.penalize_simple(penalty)` + +These correspond to different fields in the final score object, allowing you to create multi-objective scoring functions (e.g., minimizing hard violations first, then soft). + +The `penalty` argument can be a static number or a `lambda` function that receives the matched facts, allowing for dynamic penalties. + +```python +# Static penalty +.penalize_soft(50) + +# Dynamic penalty based on the fact +.penalize_soft(lambda order: order.value * 0.1) +``` + +#### Dynamic Weights + +You can change the "importance" of a constraint at runtime without rebuilding the session. + +1. **Instantiate `ConstraintWeights`** and pass it to the builder. +2. Use `session.update_constraint_weight(constraint_id, new_weight)` to change a weight. The engine will automatically and efficiently recalculate the scores for that constraint. + +```python +# --- Definition --- +from greynet import ConstraintWeights + +weights = ConstraintWeights() +builder = ConstraintBuilder(weights=weights) + +@builder.constraint("expensive_order", default_weight=1.0) +def expensive_order_rule(): + return (builder.for_each(Order) + .filter(lambda o: o.value > 1000) + .penalize_soft(lambda o: o.value / 100) + ) + +# --- Runtime --- +session = builder.build() +session.insert(Order(value=2000)) +print(session.get_score().soft_score) # Output: 20.0 ( (2000/100) * 1.0 ) + +# Now, double the importance of this rule +session.update_constraint_weight("expensive_order", 2.0) +print(session.get_score().soft_score) # Output: 40.0 ( (2000/100) * 2.0 ) +``` + +--- + +### 7. Low-Level Architecture + +While you primarily interact with the high-level API, understanding the underlying structure can be helpful. + +* **Rete Network:** When you call `builder.build()`, Greynet translates your stream definitions into a network of nodes (`FromNode`, `FilterNode`, `JoinNode`, etc.). +* **Node Sharing:** This is a key optimization. If two different constraints share a common logical path (e.g., they both start with `builder.for_each(User).filter(...)`), the engine builds the nodes for that path only once and shares them. +* **Tuples:** As facts propagate through the network, they are wrapped in `UniTuple`, `BiTuple`, etc. These internal objects track the state of a match. +* **Indexing:** Join nodes use internal hash indexes (for `EQUAL` joins) and sorted-list indexes (for range joins) to find matching tuples efficiently, avoiding full scans. +* **Scheduler:** A `BatchScheduler` collects all changes (insertions, retractions) and processes them in a queue, ensuring that changes propagate through the network in a consistent order. +* **Tuple Pooling:** To reduce garbage collection overhead in high-throughput scenarios, Greynet uses an object pool (`TuplePool`) to reuse its internal tuple objects. + +Of course. Here is the continuation of the Greynet manual, focusing on a detailed API reference and advanced usage patterns. + +### 8. The Greynet API Reference + +This section provides a quick reference for the primary classes and methods you will use when working with Greynet. + +#### `ConstraintBuilder` + +The main entry point for defining rules and building a session. + +| Method | Description | +|:------------------------------------------ |:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **`__init__(name, score_class, weights)`** | Initializes the builder. `score_class` lets you define custom score objects (e.g., `HardSoftScore`), and `weights` accepts a `ConstraintWeights` instance for dynamic penalty management. | +| **`constraint(id, weight)`** | A decorator for a function that defines a single rule. Assigns a unique ID and a default weight to the constraint. | +| **`for_each(FactClass)`** | Starts a new `Stream` of data, listening for insertions of the specified `FactClass`. This is the most common way to begin a rule definition. | +| **`build(**kwargs)`** | Compiles all the defined `@constraint` functions into an optimized Rete network and returns an executable `Session` instance. | + +#### `Session` + +The runtime engine that manages facts and calculates scores. + +| Method | Description | +|:------------------------------------------ |:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **`insert(fact)`** | Inserts a single fact into the engine and immediately processes the consequences. | +| **`retract(fact)`** | Retracts a single fact from the engine and immediately processes the consequences. | +| **`insert_batch(facts)`** | Inserts an iterable of facts. More efficient than multiple `insert()` calls. | +| **`retract_batch(facts)`** | Retracts an iterable of facts. | +| **`flush()`** | Processes all pending insertions and retractions in the queue. This is called automatically by `get_score()` and `get_constraint_matches()`. | +| **`clear()`** | Retracts all facts from the session, effectively resetting it to an empty state. | +| **`get_score()`** | Flushes the queue and returns the total aggregated score object for the current state of the facts. | +| **`get_constraint_matches()`** | Flushes the queue and returns a `dict` mapping each `constraint_id` to a list of its violations. Each violation is a tuple containing the score object and the fact(s) that caused it. | +| **`update_constraint_weight(id, weight)`** | Updates the weight multiplier for a constraint at runtime and triggers a re-calculation of all scores for that constraint. Requires a `ConstraintWeights` object to have been passed to the builder. | + +#### `Stream` + +The fluent, chainable object for defining data processing logic. + +| Method | Description | +|:------------------------------------------------ |:----------------------------------------------------------------------------------------------------------------------------- | +| **`.filter(predicate)`** | Filters tuples based on a boolean predicate. | +| **`.join(other, joiner, left_key, right_key)`** | Joins with another stream on a key. | +| **`.if_exists(other, left_key, right_key)`** | Propagates a tuple only if a match exists in the other stream. | +| **`.if_not_exists(other, left_key, right_key)`** | Propagates a tuple only if **no** match exists in the other stream. | +| **`.group_by(key_func, collector)`** | Groups tuples by a key and aggregates each group using a `Collector`. | +| **`.map(mapper_func)`** | Performs a 1-to-1 transformation of each tuple in the stream. | +| **`.flat_map(mapper_func)`** | Performs a 1-to-many transformation of each tuple in the stream. | +| **`.window(time_extractor)`** | Initiates a temporal windowing operation. Must be followed by `.sliding()` or `.tumbling()`. | +| **`.sequence(time_ext, *steps, within, ...)`** | Detects ordered sequences of facts within a time window. | +| **`.penalize_{type}(penalty)`** | Terminates a stream definition, marking it as part of a constraint. The `penalty` can be a static value or a lambda function. | + +#### `Collectors` + +A namespace of aggregation tools for use with `.group_by()`. + +| Collector | Description | +|:---------------------------------------- |:---------------------------------------------------- | +| `count()` | Counts items in the group. | +| `sum(mapper)` | Sums the numeric value returned by the mapper. | +| `avg(mapper)` | Averages the numeric value returned by the mapper. | +| `min(mapper)` / `max(mapper)` | Finds the min/max value. | +| `to_list()` / `to_set()` | Aggregates items into a list or set. | +| `compose({key: collector, ...})` | Performs multiple aggregations simultaneously. | +| `filtering(predicate, downstream)` | Pre-filters items within a group before aggregating. | +| `consecutive_sequences(seq_func)` | Finds and groups consecutive items. | +| `connected_ranges(start_func, end_func)` | Finds and merges overlapping/adjacent time ranges. | + +--- + +### 9. Advanced Patterns & Use Cases + +This section demonstrates how to combine the features of Greynet to solve more complex, real-world problems. + +#### Use Case: Resource Scheduling + +Let's model a meeting room booking system. The goal is to prevent double-bookings and to enforce a company policy that meetings should be at least 30 minutes long. + +**1. Define the Facts** + +We need a fact to represent a booking request. + +```python +from dataclasses import dataclass +from datetime import datetime, timedelta + +@dataclass +class BookingRequest: + request_id: str + room_id: str + start_time: datetime + end_time: datetime + + @property + def duration_minutes(self) -> float: + return (self.end_time - self.start_time).total_seconds() / 60 +``` + +**2. Define the Constraints** + +We'll use `Patterns` for the complex overlap detection and a simple `filter` for the duration policy. + +```python +from greynet import ConstraintBuilder, JoinerType, Patterns, Collectors + +builder = ConstraintBuilder() +patterns = Patterns(builder) # Helper for common patterns + +# Constraint 1: Prevent overlapping bookings in the same room. +# This is a critical failure, so we use penalize_hard. +@builder.constraint("overlapping_bookings") +def find_overlapping_bookings(): + # A self-join on BookingRequest is needed to compare every request with every other request. + return (builder.for_each(BookingRequest) + # Join requests that are for the same room + .join(builder.for_each(BookingRequest), + JoinerType.EQUAL, + lambda req: req.room_id, + lambda req: req.room_id) + # Filter 1: Ensure we don't match a request with itself (req1.id < req2.id is a common trick) + .filter(lambda req1, req2: req1.request_id < req2.request_id) + # Filter 2: The core overlap logic. Two ranges (s1, e1) and (s2, e2) overlap if + # the start of one is before the end of the other, and vice versa. + # Simplified: max(start1, start2) < min(end1, end2) + .filter(lambda req1, req2: max(req1.start_time, req2.start_time) < min(req1.end_time, req2.end_time)) + # Penalize each pair of overlapping bookings. + .penalize_hard(1000) + ) + +# Constraint 2: Meetings should be at least 30 minutes long. +# This is a soft policy, so we use penalize_soft. +@builder.constraint("meeting_too_short") +def find_short_meetings(): + return (builder.for_each(BookingRequest) + # Find all bookings shorter than 30 minutes. + .filter(lambda req: req.duration_minutes < 30) + # The penalty can be dynamic, penalizing more for shorter meetings. + .penalize_soft(lambda req: 30 - req.duration_minutes) + ) +``` + +**3. Run the Session** + +Now, we build the session and insert some bookings to see the rules in action. + +```python +# Build the session +scheduler_session = builder.build() + +# Create some bookings +bookings = [ + # A valid booking + BookingRequest("B1", "Room-101", datetime(2025, 7, 16, 9, 0), datetime(2025, 7, 16, 10, 0)), + # Another valid booking in a different room + BookingRequest("B2", "Room-102", datetime(2025, 7, 16, 9, 0), datetime(2025, 7, 16, 10, 0)), + # This booking overlaps with B1 + BookingRequest("B3", "Room-101", datetime(2025, 7, 16, 9, 30), datetime(2025, 7, 16, 10, 30)), + # This booking is too short (15 mins) + BookingRequest("B4", "Room-102", datetime(2025, 7, 16, 11, 0), datetime(2025, 7, 16, 11, 15)), +] + +# Insert and check score +scheduler_session.insert_batch(bookings) +score = scheduler_session.get_score() + +print(f"Hard Score (Overlaps): {score.hard_score}") +# Expected Output: Hard Score (Overlaps): 1000.0 (from B1 and B3 overlapping) + +print(f"Soft Score (Short Meetings): {score.soft_score}") +# Expected Output: Soft Score (Short Meetings): 15.0 (from B4 being 15 mins too short) + +# Get the specific violations +matches = scheduler_session.get_constraint_matches() +overlapping_pair = matches["overlapping_bookings"][0][1] # The tuple of facts +short_meeting = matches["meeting_too_short"][0][1][0] # The single fact + +print(f"Overlap detected between {overlapping_pair[0].request_id} and {overlapping_pair[1].request_id}") +print(f"Short meeting detected: {short_meeting.request_id}") +``` + +This example illustrates how separate constraints can work together to validate a complex system, combining `join`, `filter`, and different penalty levels to enforce both critical rules and soft policies. diff --git a/greyjack/greyjack/score_calculation/greynet/docs/manual_2.md b/greyjack/greyjack/score_calculation/greynet/docs/manual_2.md new file mode 100644 index 0000000..d5f79f0 --- /dev/null +++ b/greyjack/greyjack/score_calculation/greynet/docs/manual_2.md @@ -0,0 +1,885 @@ +Of course. Here is a comprehensive reference guide to the Greynet engine, structured in a top-down manner. + +*** + +# Greynet Reference Manual + +Welcome to the official reference manual for Greynet, a high-performance, forward-chaining rules engine for Python. This document provides a top-down exploration of the engine, from high-level concepts to the intricacies of its internal architecture. + +## Table of Contents + +1. **Introduction to Greynet** + * What is Greynet? + * Core Concepts + * A First Example: "Hello, Greynet" +2. **The Constraint Lifecycle** + * `ConstraintBuilder`: The Rule Architect + * `Session`: The Runtime Engine + * Scoring and Weights +3. **The Stream API: Defining Logic** + * Starting a Stream + * Filtering & Transformation + * Joining Streams + * Aggregation with `group_by` + * Conditional Logic +4. **Advanced Stream Operations** + * Temporal Windowing + * Sequential Pattern Matching +5. **The Collector Toolkit** + * Basic Aggregators + * Compositional Collectors + * Specialized Collectors +6. **Under the Hood: The Rete Network** + * Core Principles + * Anatomy of the Network (Node Types) + * Data Flow and Memory Management + +--- + +## 1. Introduction to Greynet + +This section covers the fundamental principles of Greynet, its core components, and a simple example to get you started. + +### What is Greynet? + +Greynet is a declarative rules engine designed for solving complex event processing and constraint satisfaction problems. It is built upon the principles of the **Rete algorithm**, which provides a highly efficient method for matching a large collection of facts against a large collection of rules. + +Its key features include: + +* **Declarative, Fluent API**: Define complex rules through a clean, chainable `Stream` API. +* **High Performance**: Utilizes the Rete algorithm, node sharing, and object pooling to minimize redundant calculations and memory overhead. +* **Dynamic Configuration**: Constraint penalties can be updated at runtime, allowing for immediate re-evaluation of the problem space without rebuilding the engine. +* **Rich Functionality**: Natively supports complex joins, aggregations, conditional logic (`exists`/`not exists`), and advanced temporal/sequential pattern matching. + +### Core Concepts + +Understanding these four concepts is key to using Greynet effectively. + +* #### **Fact** + + A Fact is any plain Python object (often a `dataclass`) that represents a piece of data in the system. Facts are the "what" that your rules operate on. For example, a `RoomBooking` or a `UserLoginEvent`. + +* #### **Stream** + + A Stream is a fluent, declarative API for defining the logic of a single rule. You start a stream from a collection of facts and apply a series of operations like `filter()`, `join()`, and `group_by()` to define the conditions for a constraint violation. + +* #### **Constraint** + + A Constraint is a specific rule that, when its conditions are met, applies a penalty to the overall score. It is the terminal operation of a Stream, defined by methods like `penalize_hard()`. + +* #### **Session** + + The Session is the runtime environment for the Greynet engine. It holds all the facts, manages the Rete network, and calculates the total score. You interact with the session by inserting, retracting, and updating facts. + +### A First Example: "Hello, Greynet" + +Let's model a simple scheduling rule: **Two meetings cannot overlap in the same room.** + +* #### **Step 1: Define the Fact** + + We need a way to represent a meeting. A `dataclass` is perfect for this. + + ```python + from dataclasses import dataclass + from datetime import datetime + + @greynet_fact +@dataclass() + class Meeting: + id: str + room: str + start_time: datetime + end_time: datetime + ``` + +* #### **Step 2: Define the Constraint** + + We use `ConstraintBuilder` to define our rule set. The rule logic is defined using the Stream API. + + ```python + from greynet import ConstraintBuilder, JoinerType + + # Initialize the builder + builder = ConstraintBuilder(name="scheduling_rules") + + # The @constraint decorator registers the rule + @builder.constraint("Overlapping Meetings") + def overlapping_meetings(): + # Start two streams from the Meeting fact + meetings1 = builder.for_each(Meeting) + meetings2 = builder.for_each(Meeting) + + return ( + # 1. Join meetings with themselves if they are in the same room + meetings1.join(meetings2, + JoinerType.EQUAL, + lambda m: m.room, # Left key + lambda m: m.room # Right key + ) + # 2. Ensure we don't match a meeting with itself or get duplicates (m1, m2) and (m2, m1) + .filter(lambda m1, m2: m1.id < m2.id) + # 3. Filter for pairs that actually overlap in time + .filter(lambda m1, m2: max(m1.start_time, m2.start_time) < min(m1.end_time, m2.end_time)) + # 4. If all conditions are met, apply a penalty + .penalize_hard(1) + ) + ``` + +* #### **Step 3: Build the Session and Interact** + + Once the rules are defined, we build the session and can start inserting facts. + + ```python + from datetime import datetime, timedelta + + # Build the session from the defined constraints + session = builder.build() + + # Define some meetings + m1 = Meeting("m1", "Room A", datetime(2025, 7, 16, 9), datetime(2025, 7, 16, 10)) + m2 = Meeting("m2", "Room B", datetime(2025, 7, 16, 9), datetime(2025, 7, 16, 10)) + m3 = Meeting("m3", "Room A", datetime(2025, 7, 16, 9, 30), datetime(2025, 7, 16, 10, 30)) # Overlaps with m1 + + # Insert facts into the session + session.insert_batch([m1, m2, m3]) + + # Get the total score. The score object shows the penalty from the violation. + score = session.get_score() + print(f"Total Score: {score}") # Output: Total Score: + + # Get a detailed breakdown of which facts violated the constraint + matches = session.get_constraint_matches() + print(f"Violations: {matches}") + # Output: Violations: {'Overlapping Meetings': [(, )]} + ``` + +This example demonstrates the core workflow: define facts, build rules with a fluent API, create a session, and use it to evaluate your data. + +--- + +## 2. The Constraint Lifecycle + +This section covers the main components responsible for defining, building, and executing constraints. + +### `ConstraintBuilder`: The Rule Architect + +The `ConstraintBuilder` is the factory for creating a `Session`. It collects all your rule definitions before compiling them into an efficient Rete network. + +#### Key Methods + +* #### `ConstraintBuilder(name, score_class=SimpleScore, weights=None)` + + * `name`: A descriptive name for the rule package. + * `score_class`: The class used to represent the score (e.g., `SimpleScore`, `HardSoftScore`). It must have a static `get_score_fields()` method. + * `weights`: An optional `ConstraintWeights` object to enable dynamic, runtime updates to penalties. + +* #### `@builder.constraint(constraint_id, default_weight=1.0)` + + This decorator registers a function as a constraint definition. + + * `constraint_id`: A unique string identifier for the rule. + * `default_weight`: A default multiplier for the penalty. + The decorated function must return a `Constraint` object, which is created by calling a `.penalize_*()` method on a stream. + +* #### `builder.for_each(fact_class)` + + This is the entry point for every rule. It creates a `Stream` that will emit any facts of the given `fact_class` that are inserted into the session. + +* #### `builder.build(**kwargs)` + + This method compiles all the registered constraints into a `Session`. It builds the Rete network, sets up node sharing, and prepares the session for execution. + +### `Session`: The Runtime Engine + +The `Session` is your primary interface for interacting with the rules engine at runtime. + +#### Key Methods + +* #### `insert(fact)` / `insert_batch(facts)` + + Adds one or more facts to the session's working memory, triggering the rule evaluation process. + +* #### `retract(fact)` / `retract_batch(facts)` + + Removes one or more facts from working memory, reversing any consequences of their previous insertion. + +* #### `flush()` + + Greynet uses a scheduler to batch changes for efficiency. `flush()` forces the immediate processing of all pending insertions and retractions. Methods like `get_score()` and `get_constraint_matches()` call `flush()` implicitly. + +* #### `get_score()` + + Calculates and returns the total aggregated score from all constraint violations. The type of the returned object is determined by the `score_class` set in the `ConstraintBuilder`. + +* #### `get_constraint_matches()` + + Returns a dictionary detailing every constraint violation. The keys are the `constraint_id`s, and the values are lists of tuples, where each tuple contains the score object and the fact(s) that caused the violation. + +* #### `update_constraint_weight(constraint_id, new_weight)` + + If the session was built with a `ConstraintWeights` object, this method allows you to change the penalty multiplier for a specific constraint at runtime. The engine will automatically and efficiently recalculate the scores for all existing matches of that constraint. + +* #### `clear()` + + Retracts all known facts from the session, effectively resetting it to an empty state. + +### Scoring and Weights + +The final step of any rule definition is to specify *how* a violation should be scored. + +* #### Penalty Methods + + Every `Stream` has penalty methods that terminate the stream and create a `Constraint` object. + + * `penalize_hard(...)` + * `penalize_medium(...)` + * `penalize_soft(...)` + * `penalize_simple(...)` + The names correspond to different fields in a score object (e.g., `HardSoftScore`), allowing you to categorize penalties. The `penalize_simple` method targets the `simple_value` field in the default `SimpleScore`. + +* #### Dynamic Penalties + + The argument to a penalty method can be a static number or a callable `lambda` function. If it's a lambda, it will be executed with the matching facts as arguments, allowing you to calculate a penalty based on the data itself. + + ```python + # Example: Penalty scales with the duration of the overlap + from datetime import timedelta + + .penalize_soft(lambda m1, m2: (min(m1.end_time, m2.end_time) - max(m1.start_time, m2.start_time)).total_seconds()) + ``` + +* #### `ConstraintWeights` + + This thread-safe class manages the weight multipliers for each constraint. When you call `update_constraint_weight` on the session, you are modifying a shared `ConstraintWeights` object. The final penalty for any violation is calculated as: + $$ \text{final\_penalty} = \text{base\_penalty\_value} \times \text{dynamic\_weight} $$ + +This concludes the high-level overview of defining and running constraints. The next section will dive deep into the Stream API, which forms the core of the rule definition logic. + +--- + +*(This response is the first part of a multi-part guide. Subsequent parts will cover the Stream API, advanced features, and internal architecture in detail.)* + +Of course. Here is the next part of the Greynet Reference Manual, focusing on the powerful Stream API. + +*** + +## 3. The Stream API: Defining Logic + +The Stream API is the heart of Greynet's declarative rule engine. It provides a fluent, chainable interface to express complex logic, starting from raw facts and progressively filtering, transforming, joining, and aggregating them until a specific condition for a constraint violation is met. + +Each operation in the chain creates a new `Stream` object, representing a new state in the data processing pipeline. Under the hood, Greynet translates this chain of streams into an efficient network of nodes. + +### Visualizing a Stream + +A simple stream can be visualized as a data flow pipeline: + +```mermaid +graph TD + A[for_each(Meeting)] --> B(filter); + B --> C(penalize_hard); + subgraph Stream Definition + direction LR + A + B + C + end +``` + +### Starting a Stream + +All rule logic begins by creating a stream from a source of facts. + +* #### `builder.for_each(FactClass)` + + This method creates the initial `Stream`. It will be populated with every object of `FactClass` that is inserted into the session. The stream's elements are `UniTuple` objects, each containing a single fact. + + ```python + # Creates a stream of UniTuple + meetings_stream = builder.for_each(Meeting) + ``` + +### Filtering & Transformation + +These operations modify the elements within a single stream. + +* #### `stream.filter(predicate)` + + The most common operation. It filters the stream, only allowing tuples that satisfy the `predicate` to pass through. The `predicate` is a `lambda` function that receives the contents of the tuple as arguments. + + ```python + # Before filter: Stream of all meetings + # After filter: Stream of meetings in Room A only + meetings_in_room_a = builder.for_each(Meeting).filter(lambda m: m.room == "Room A") + ``` + +* #### `stream.map(mapper)` + + Transforms each element in the stream into a *single new element*. The result is a new `Stream` of `UniTuple` objects containing the mapped elements. + + ```python + # Before map: Stream of Meeting objects + # After map: Stream of strings (room names) + room_names = builder.for_each(Meeting).map(lambda m: m.room) + ``` + +* #### `stream.flat_map(mapper)` + + A more powerful version of `map`. It transforms each element into an *iterable* of new elements. The engine then flattens all the generated iterables into a single output stream. + + ```python + @dataclass + class Team: + name: str + members: list[str] + + # Before flat_map: Stream of Team objects + # After flat_map: Stream of strings (individual member names) + all_members = builder.for_each(Team).flat_map(lambda t: t.members) + ``` + +### Joining Streams + +Joins are fundamental to finding relationships between different facts. Greynet supports a variety of join types. + +* #### `stream.join(other_stream, joiner_type, left_key_func, right_key_func)` + + Combines two streams into one. A new combined tuple is created for each pair of tuples (one from the left stream, one from the right) that satisfies the join condition. + + * `other_stream`: The stream to join with. + * `joiner_type`: An enum from `greynet.common.joiner_type.JoinerType`. + * `left_key_func` / `right_key_func`: Lambda functions that extract the join key from a tuple in the left/right stream, respectively. + + The arity of the resulting stream is the sum of the arities of the input streams. For example, joining two `UniTuple` streams results in a `BiTuple` stream. + + #### Common `JoinerType` values: + + * `EQUAL`: The default and most common join type. + * `NOT_EQUAL` + * `LESS_THAN`, `GREATER_THAN`, etc. + * `RANGE_OVERLAPS`: For joining on time intervals or numeric ranges. + + ```python + @greynet_fact +@dataclass() + class Room: + name: str + capacity: int + + # Join Meetings with Rooms to find over-capacity meetings + overbooked = ( + builder.for_each(Meeting) + .join(builder.for_each(Room), + JoinerType.EQUAL, + lambda m: m.room, # Key from Meeting stream + lambda r: r.name # Key from Room stream + ) + # The resulting stream contains BiTuple(meeting, room) + .filter(lambda meeting, room: meeting.attendee_count > room.capacity) + .penalize_hard(1) + ) + ``` + +### Aggregation with `group_by` + +`group_by` is used to aggregate facts that share a common key. This is the foundation for rules like "a user cannot have more than 3 active sessions" or "calculate the average transaction value per customer." + +* #### `stream.group_by(group_key_function, collector_supplier)` + + This operation collapses a stream into a new `Stream` of `BiTuple` objects. + + * `group_key_function`: A lambda that extracts the grouping key from each fact. + * `collector_supplier`: A function that supplies a **Collector** instance. The collector defines *how* the facts within each group are aggregated. + + The output stream contains `BiTuple`s where: + + * `fact_a` is the group key. + * `fact_b` is the result of the aggregation from the collector. + + ```python + from greynet import Collectors + + # Rule: Any user with more than 3 logins is flagged. + too_many_logins = ( + builder.for_each(UserLoginEvent) + .group_by( + lambda event: event.user_id, # Group by the user's ID + Collectors.count() # The aggregation is a simple count + ) + # The stream now contains BiTuple(user_id, count) + .filter(lambda user_id, count: count > 3) + .penalize_soft(1) + ) + ``` + + The `Collectors` class provides a rich toolkit for various aggregations, covered in Section 5. + +### Conditional Logic (`exists` / `not exists`) + +These powerful operations allow you to express rules that depend on the presence or absence of other facts, without needing to perform a full join. + +* #### `stream.if_exists(other_stream, left_key, right_key)` + + Propagates a tuple from the original stream **only if** at least one matching fact exists in `other_stream` based on the provided keys. The output stream has the same arity and content as the original stream. + +* #### `stream.if_not_exists(other_stream, left_key, right_key)` + + The inverse of `if_exists`. Propagates a tuple **only if** no matching facts exist in `other_stream`. + + ```python + @greynet_fact +@dataclass() + class Order: + id: str + customer_id: str + + @greynet_fact +@dataclass() + class Payment: + order_id: str + customer_id: str + + # Find all orders that do NOT have a corresponding payment. + unpaid_orders = ( + builder.for_each(Order) + .if_not_exists( + builder.for_each(Payment), + left_key=lambda order: order.id, # Key from Order stream + right_key=lambda pmt: pmt.order_id # Key from Payment stream + ) + .penalize_medium(1) + ) + ``` + +This concludes the core Stream API. The next section will explore the advanced operations for handling temporal and sequential data. + +--- + +*(This response is the second part of a multi-part guide. Subsequent parts will cover advanced streams, collectors, and the internal architecture.)* + +Of course. Here is the third part of the Greynet Reference Manual, detailing the advanced stream operations for temporal and sequential analysis. + +*** + +## 4. Advanced Stream Operations + +While the core API covers a vast range of use cases, modern systems often require analysis based on time and the order of events. Greynet provides powerful, dedicated stream operations for these scenarios. These operations can only be applied to a `Stream` of `UniTuple` (a stream of single facts). + +### Temporal Windowing + +Windowing is the process of grouping facts into time-based buckets. This is essential for time-series analysis, such as calculating moving averages or detecting spikes in activity. + +The windowing process starts with the `.window()` method, which requires a `time_extractor` function to tell the engine how to get a `datetime` object from your fact. This method returns a special `WindowedStream` object, which then allows you to specify the type of window. + +* #### `stream.window(time_extractor).sliding(size, slide)` + + Creates overlapping windows. + + * `size`: A `timedelta` defining the total duration of each window. + * `slide`: A `timedelta` defining how far the window moves forward for each step. For a sliding window, `slide` must be less than `size`. + + The output is a `Stream` of `BiTuple`s, where for each window: + + * `fact_a` is the `datetime` object representing the window's start time. + * `fact_b` is a `list` of all facts that fall within that window. + + ##### Example: Detect more than 10 API calls from a single IP in any 1-minute interval. + + ```python + from greynet import Collectors + from datetime import timedelta + + @greynet_fact +@dataclass() + class ApiCall: + ip_address: str + timestamp: datetime + + # The rule definition + too_many_requests = ( + builder.for_each(ApiCall) + # First, group calls by IP address + .group_by( + lambda call: call.ip_address, + Collectors.to_list() # Collect all calls for each IP + ) + # The stream is now BiTuple(ip_address, [list_of_calls]) + # Flatten the list of calls into a stream of individual calls for windowing + .flat_map(lambda ip, calls: calls) + # Now, apply a sliding window to the stream of individual calls + .window(time_extractor=lambda call: call.timestamp) + .sliding(size=timedelta(minutes=1), slide=timedelta(seconds=10)) + # The stream is now BiTuple(window_start_time, [calls_in_window]) + # We only care about windows with more than 10 calls + .filter(lambda window_start, calls: len(calls) > 10) + .penalize_soft(1) + ) + ``` + +* #### `stream.window(time_extractor).tumbling(size)` + + Creates fixed, non-overlapping windows. Each fact belongs to exactly one window. This is equivalent to a sliding window where `slide == size`. + + * `size`: A `timedelta` defining the duration of each window. + + The output format is identical to that of a sliding window. + + ##### Example: Calculate the total value of transactions per hour. + + ```python + # Fact definition + @greynet_fact +@dataclass() + class Transaction: + id: str + amount: float + timestamp: datetime + + # Rule Definition + hourly_transaction_value = ( + builder.for_each(Transaction) + .window(time_extractor=lambda tx: tx.timestamp) + .tumbling(size=timedelta(hours=1)) + # Stream is BiTuple(hour_start_time, [transactions_in_hour]) + # Now we can map this to calculate the sum + .map(lambda hour_start, transactions: (hour_start, sum(tx.amount for tx in transactions))) + # This stream can now be used for other rules, e.g., flagging hours with unusually high volume. + ) + ``` + +### Sequential Pattern Matching + +Sometimes, the specific *order* of events is more important than their aggregate count. The `.sequence()` method is designed to detect complex patterns of events occurring in a specific chronological order within a given time frame. + +* #### `stream.sequence(time_extractor, *steps, within, allow_gaps=True)` + + Finds sequences of facts that match an ordered series of conditions. + + * `time_extractor`: A function to get a `datetime` from the fact. + * `*steps`: A variable number of predicate functions. Each function defines a condition for one step in the sequence. + * `within`: A `timedelta` specifying the maximum allowed duration between the timestamp of the *first* fact in the sequence and the *last* fact. + * `allow_gaps`: If `True` (default), other events that don't match the pattern can occur between the steps. If `False`, the sequence must be composed of consecutive matching events. + + The output is a `Stream` of `UniTuple`s. For each complete sequence found, the tuple's single fact is a `list` containing the facts that formed the sequence, in chronological order. + + ##### Example: Detect suspicious behavior (a user logs in, a critical action fails, and they immediately log out). + + ```python + # Fact definitions + @greynet_fact +@dataclass() class Login: user: str; timestamp: datetime + @greynet_fact +@dataclass() class ActionFailed: user: str; action: str; timestamp: datetime + @greynet_fact +@dataclass() class Logout: user: str; timestamp: datetime + + # A helper stream to unify all event types + all_events = builder.for_each(object) # Matches any fact + + # The sequence rule + suspicious_sequence = ( + all_events + .sequence( + # 1. Time Extractor + time_extractor=lambda e: e.timestamp, + # 2. Steps + lambda e: isinstance(e, Login), + lambda e: isinstance(e, ActionFailed) and e.action == "update_credentials", + lambda e: isinstance(e, Logout), + # 3. Time Window + within=timedelta(minutes=2) + ) + # Stream contains UniTuple([login_event, failed_event, logout_event]) + # Filter for sequences where all events are for the same user + .filter(lambda event_list: len(set(e.user for e in event_list)) == 1) + .penalize_hard(10) + ) + ``` + +These advanced features enable Greynet to solve a class of problems that are difficult or impossible to express with simple joins and filters, making it a versatile tool for real-time monitoring and complex event processing. + +--- + +*(This response is the third part of a multi-part guide. The next part will cover the rich Collector Toolkit used with `group_by`.)* + +Of course. Here is the next part of the Greynet Reference Manual, focusing on the versatile Collector Toolkit. + +*** + +## 5. The Collector Toolkit + +When you use the `group_by` operation, you need to specify *how* the facts within each group should be aggregated. This is the job of a **Collector**. Greynet provides a rich set of pre-built collectors, accessible through the `greynet.Collectors` helper class. + +Collectors are supplied as functions to `group_by` (e.g., `Collectors.count()`, not `Collectors.count`). This is because the engine needs to create a new instance of the collector for each group that is formed. + +### Basic Aggregators + +These are the most frequently used collectors for common aggregation tasks. + +* #### `Collectors.count()` + + Simply counts the number of items in the group. The result is an integer. + + ```python + # Count the number of tasks per project + tasks_per_project = builder.for_each(Task).group_by( + lambda task: task.project_id, + Collectors.count() + ) + # Resulting stream: BiTuple(project_id, count_of_tasks) + ``` + +* #### `Collectors.sum(mapping_function)` + + Calculates the sum of the values extracted by the `mapping_function`. The result is a number. + + ```python + # Calculate the total sales amount per region + sales_per_region = builder.for_each(Sale).group_by( + lambda sale: sale.region, + Collectors.sum(lambda sale: sale.amount) + ) + # Resulting stream: BiTuple(region, total_sales_amount) + ``` + +* #### `Collectors.avg(mapping_function)` + + Calculates the average of the values extracted by the `mapping_function`. The result is a float. + +* #### `Collectors.min(mapping_function)` / `Collectors.max(mapping_function)` + + Finds the minimum or maximum of the values extracted by the `mapping_function`. + +* #### `Collectors.to_list()` + + Collects all items in the group into a `list`. + +* #### `Collectors.to_set()` + + Collects all unique items in the group into a `set`. + +* #### `Collectors.distinct()` + + Collects all unique items in the group into a `list`, preserving the order of insertion. + +### Compositional Collectors + +Sometimes you need to perform multiple aggregations on the same group. Instead of creating multiple `group_by` streams, you can use `compose` to do it all in one efficient pass. + +* #### `Collectors.compose(collector_suppliers_dict)` + + Takes a dictionary where keys are descriptive names and values are other collector suppliers. The result of the aggregation is a dictionary containing the results of each sub-collector. + + ##### Example: For each project, get the task count, total budget, and a list of unique assignees. + + ```python + project_summary = builder.for_each(Task).group_by( + lambda task: task.project_id, + Collectors.compose({ + 'task_count': Collectors.count(), + 'total_budget': Collectors.sum(lambda t: t.cost), + 'assignees': Collectors.distinct(lambda t: t.assignee) + }) + ) + # Resulting stream: BiTuple(project_id, summary_dict) + # e.g., ('P-101', {'task_count': 5, 'total_budget': 15000, 'assignees': ['Alice', 'Bob']}) + + # You can then filter based on the composed result + high_cost_projects = project_summary.filter( + lambda proj_id, summary: summary['total_budget'] > 50000 + ) + ``` + +### Specialized Collectors + +These collectors provide more advanced or targeted functionality. + +* #### `Collectors.filtering(predicate, downstream_supplier)` + + Filters items *within* a group before passing them to a downstream collector. + + ##### Example: Count only the 'High Priority' tasks within each project. + + ```python + urgent_task_count = builder.for_each(Task).group_by( + lambda task: task.project_id, + Collectors.filtering( + lambda task: task.priority == "High", # The filter to apply + Collectors.count() # The collector for items that pass + ) + ) + # Resulting stream: BiTuple(project_id, count_of_high_priority_tasks) + ``` + +* #### `Collectors.mapping(mapping_function, downstream_supplier)` + + Applies a transformation to items *within* a group before passing them to a downstream collector. + + ##### Example: Calculate the average length of task descriptions per project. + + ```python + avg_desc_length = builder.for_each(Task).group_by( + lambda task: task.project_id, + Collectors.mapping( + lambda task: len(task.description), # The mapping to apply + Collectors.avg(lambda length: length) # The collector for the mapped values + ) + ) + ``` + +### Advanced Data Structure Collectors + +These collectors aggregate items into sophisticated data structures for specialized use cases. + +* #### `Collectors.to_bloom_filter(estimated_items, false_positive_rate)` + + Aggregates items into a `CountingBloomFilter`, a probabilistic data structure that is highly memory-efficient for checking set membership. It's useful when groups are very large and you only need to ask "is this item *probably* in the group?". + +* #### `Collectors.consecutive_sequences(sequence_func)` + + Tracks and identifies runs of consecutive items. It's perfect for finding things like consecutive login days, unbroken streaks, or adjacent seat bookings. + + * `sequence_func`: A function that extracts a value (like a number or date) from the fact to check for consecutiveness. + + ##### Example: Find groups of consecutively numbered tickets. + + ```python + # Fact: Ticket(id=1), Ticket(id=2), Ticket(id=4), Ticket(id=5) + consecutive_tickets = builder.for_each(Ticket).group_by( + lambda t: "all_tickets", # Group all tickets together + Collectors.consecutive_sequences(lambda ticket: ticket.id) + ) + # Result: [ConsecutiveSequence(start=1, end=2, ...), ConsecutiveSequence(start=4, end=5, ...)] + ``` + +* #### `Collectors.connected_ranges(start_func, end_func)` + + Takes items that represent a range (e.g., a time interval) and merges any ranges that overlap or are immediately adjacent. + + * `start_func` / `end_func`: Functions to extract the start and end of the range from a fact. + + ##### Example: Find the actual busy time slots for a meeting room. + + ```python + # Facts: Booking(start=9, end=10), Booking(start=9:30, end=10:30) + busy_slots = builder.for_each(Booking).group_by( + lambda b: b.room_id, + Collectors.connected_ranges( + start_func=lambda b: b.start_time, + end_func=lambda b: b.end_time + ) + ) + # Result: [ConnectedRange(start=9, end=10:30, ...)] + ``` + +This comprehensive toolkit allows `group_by` to be one of the most powerful and flexible operations in Greynet, enabling a wide array of stateful aggregations. + +--- + +*(This response is the fourth part of a multi-part guide. The final part will provide a glimpse into the engine's internal Rete network architecture.)* + +Of course. Here is the final part of the Greynet Reference Manual, which delves into the engine's internal architecture. + +*** + +## 6. Under the Hood: The Rete Network + +The declarative Stream API provides a simple and powerful way to define rules. This simplicity is made possible by a sophisticated underlying engine that translates your stream definitions into a highly optimized data-flow graph known as a **Rete network**. Understanding the basics of this network can help you appreciate why Greynet is so efficient and how it handles data. + +### Core Principles of Rete + +The Rete algorithm (from the Latin word for "net") is designed to solve a many-to-many matching problem (many facts vs. many rules) efficiently. Its key principles are: + +1. **Statefulness**: The network maintains the state of partial matches. When a new fact is added, it doesn't re-evaluate everything from scratch. It only calculates the *delta*—the new matches created or old matches invalidated by the change. +2. **Node Sharing**: If multiple rules share a common condition (e.g., `filter(lambda m: m.room == "Room A")`), the network builds the corresponding node for that condition only once and shares it across all relevant rule paths. This drastically reduces memory usage and redundant computation. +3. **Data-Driven Execution**: The flow of data (facts) through the network triggers the evaluation. There is no central loop that iterates through rules. + +### Anatomy of the Network + +The Rete network in Greynet is composed of different types of nodes, each corresponding to an operation in the Stream API. + +```mermaid +graph TD + subgraph Alpha Network (Single-Fact Conditions) + direction LR + F1[FromUniNode
Meeting] --> P1(FilterNode
room == 'A'); + end + + subgraph Beta Network (Multi-Fact Conditions) + direction TB + F2[FromUniNode
Room] --> J1{JoinNode
meeting.room == room.name}; + P1 -->|Left Input| J1; + F2 -->|Right Input| J1; + J1 --> G1[GroupNode
by meeting.day]; + G1 --> S1[ScoringNode
'Too Many Meetings']; + end + + style F1 fill:#cde4f9,stroke:#333 + style F2 fill:#cde4f9,stroke:#333 + style P1 fill:#d5e8d4,stroke:#333 + style J1 fill:#ffe6cc,stroke:#333 + style G1 fill:#ffe6cc,stroke:#333 + style S1 fill:#f8cecc,stroke:#333 +``` + +*A simplified Rete graph showing different node types.* + +#### The Alpha Network: Simple Conditions + +These nodes handle conditions that apply to a single fact. + +* #### `FromUniNode` + + The entry point for all facts into the network. There is one `FromUniNode` for each fact class you use in `builder.for_each()`. It takes a raw fact and wraps it in a `UniTuple`. + +* #### `FilterNode` + + Represents a `stream.filter()` operation. It receives a tuple, applies its predicate, and propagates the tuple downstream only if the predicate returns `True`. + +#### The Beta Network: Complex Conditions + +These nodes handle relationships between multiple facts. They are the core of the network's stateful memory. + +* #### `JoinNode` + + Corresponds to `stream.join()`. A `JoinNode` has two inputs (left and right) and maintains an indexed memory for each. When a tuple arrives on the left, it probes the right-side memory for matches based on the join key, creating new, larger tuples for each match. This is far more efficient than a nested loop over all facts. + +* #### `ConditionalNode` + + Implements `if_exists` and `if_not_exists`. It works like a `JoinNode` but instead of creating new combined tuples, it simply checks for the existence of a match in its right-side memory and decides whether to propagate its original left-side tuple. + +* #### `GroupNode` + + Represents `group_by`. This is a highly stateful node that maintains a mapping from group keys to **Collector** instances. When a fact arrives, it finds the correct collector, updates its state, and emits a new tuple with the key and the collector's latest result. + +* #### Temporal & Sequential Nodes (`SlidingWindowNode`, `SequencePatternNode`) + + These are highly specialized beta-like nodes that implement the logic for `window()` and `sequence()` operations. They maintain complex internal state, such as sorted lists of events and active time windows, to efficiently detect temporal patterns. + +#### Terminal Nodes + +* #### `ScoringNode` + + The final node in a constraint's path. It is created by a `.penalize_*()` call. When a tuple reaches a `ScoringNode`, it signifies that all conditions for a constraint violation have been met. The node then: + 1. Executes the impact function to calculate the penalty score. + 2. Stores the tuple and its score as a "constraint match". + The `session.get_score()` and `session.get_constraint_matches()` methods simply query the internal state of these `ScoringNode`s. + +### Data Flow and Memory Management + +* #### **Tuples** + + Data does not flow through the network as raw facts. It flows as **Tuple** objects (`UniTuple`, `BiTuple`, `TriTuple`, etc.). A tuple represents a partial match for a rule. For example, a `BiTuple` that has passed through a `JoinNode` represents a successful pairing of two initial facts. + +* #### **The Scheduler** + + To avoid redundant work, Greynet does not process changes instantly. Insertions, retractions, and updates are placed in a queue managed by the `BatchScheduler`. When `session.flush()` is called (or triggered by `get_score()`), the scheduler processes all pending changes in an orderly fashion, ensuring that each change is propagated through the network exactly once. + +* #### **Tuple Pooling** + + Creating and destroying thousands of `Tuple` objects can be slow due to memory allocation overhead. Greynet uses a `TuplePool` to mitigate this. When a tuple is no longer needed (e.g., a fact is retracted and its corresponding match is invalidated), the tuple object is not destroyed but is reset and returned to the pool for later reuse. This significantly improves performance in dynamic scenarios with many insertions and retractions. + +By abstracting this intricate network behind the clean Stream API, Greynet offers the best of both worlds: a simple, declarative interface for rule definition and a highly efficient, stateful execution engine for high-performance evaluation. + +*** + +This concludes the Greynet Reference Manual. You now have a comprehensive understanding of the engine, from its high-level concepts down to its internal workings. diff --git a/greyjack/greyjack/score_calculation/greynet/function/__init__.py b/greyjack/greyjack/score_calculation/greynet/function/__init__.py new file mode 100644 index 0000000..57ee854 --- /dev/null +++ b/greyjack/greyjack/score_calculation/greynet/function/__init__.py @@ -0,0 +1,11 @@ +# greynet/function/__init__.py +from ..function.function import Function +from ..function.predicate import Predicate +from ..function.bi_function import BiFunction +from ..function.bi_predicate import BiPredicate +from ..function.tri_function import TriFunction +from ..function.tri_predicate import TriPredicate +from ..function.quad_function import QuadFunction +from ..function.quad_predicate import QuadPredicate +from ..function.penta_predicate import PentaPredicate +from ..function.supplier import Supplier \ No newline at end of file diff --git a/greyjack/greyjack/score_calculation/greynet/function/bi_function.py b/greyjack/greyjack/score_calculation/greynet/function/bi_function.py new file mode 100644 index 0000000..b4ecdec --- /dev/null +++ b/greyjack/greyjack/score_calculation/greynet/function/bi_function.py @@ -0,0 +1,7 @@ +# greynet/function/bi_function.py +from abc import ABC, abstractmethod + +class BiFunction(ABC): + @abstractmethod + def apply(self, a, b): + pass \ No newline at end of file diff --git a/greyjack/greyjack/score_calculation/greynet/function/bi_predicate.py b/greyjack/greyjack/score_calculation/greynet/function/bi_predicate.py new file mode 100644 index 0000000..a4aca47 --- /dev/null +++ b/greyjack/greyjack/score_calculation/greynet/function/bi_predicate.py @@ -0,0 +1,7 @@ +# greynet/function/bi_predicate.py +from abc import ABC, abstractmethod + +class BiPredicate(ABC): + @abstractmethod + def test(self, a, b): + pass \ No newline at end of file diff --git a/greyjack/greyjack/score_calculation/greynet/function/function.py b/greyjack/greyjack/score_calculation/greynet/function/function.py new file mode 100644 index 0000000..c92cc64 --- /dev/null +++ b/greyjack/greyjack/score_calculation/greynet/function/function.py @@ -0,0 +1,7 @@ +# greynet/function/function.py +from abc import ABC, abstractmethod + +class Function(ABC): + @abstractmethod + def apply(self, value): + pass \ No newline at end of file diff --git a/greyjack/greyjack/score_calculation/greynet/function/penta_predicate.py b/greyjack/greyjack/score_calculation/greynet/function/penta_predicate.py new file mode 100644 index 0000000..bf912a1 --- /dev/null +++ b/greyjack/greyjack/score_calculation/greynet/function/penta_predicate.py @@ -0,0 +1,7 @@ +# greynet/function/penta_predicate.py +from abc import ABC, abstractmethod + +class PentaPredicate(ABC): + @abstractmethod + def test(self, a, b, c, d, e): + pass diff --git a/greyjack/greyjack/score_calculation/greynet/function/predicate.py b/greyjack/greyjack/score_calculation/greynet/function/predicate.py new file mode 100644 index 0000000..b711854 --- /dev/null +++ b/greyjack/greyjack/score_calculation/greynet/function/predicate.py @@ -0,0 +1,7 @@ +# greynet/function/predicate.py +from abc import ABC, abstractmethod + +class Predicate(ABC): + @abstractmethod + def test(self, value): + pass \ No newline at end of file diff --git a/greyjack/greyjack/score_calculation/greynet/function/quad_function.py b/greyjack/greyjack/score_calculation/greynet/function/quad_function.py new file mode 100644 index 0000000..6551c70 --- /dev/null +++ b/greyjack/greyjack/score_calculation/greynet/function/quad_function.py @@ -0,0 +1,7 @@ +# greynet/function/quad_function.py +from abc import ABC, abstractmethod + +class QuadFunction(ABC): + @abstractmethod + def apply(self, a, b, c, d): + pass \ No newline at end of file diff --git a/greyjack/greyjack/score_calculation/greynet/function/quad_predicate.py b/greyjack/greyjack/score_calculation/greynet/function/quad_predicate.py new file mode 100644 index 0000000..4ef3afd --- /dev/null +++ b/greyjack/greyjack/score_calculation/greynet/function/quad_predicate.py @@ -0,0 +1,7 @@ +# greynet/function/quad_predicate.py +from abc import ABC, abstractmethod + +class QuadPredicate(ABC): + @abstractmethod + def test(self, a, b, c, d): + pass \ No newline at end of file diff --git a/greyjack/greyjack/score_calculation/greynet/function/supplier.py b/greyjack/greyjack/score_calculation/greynet/function/supplier.py new file mode 100644 index 0000000..a5756c8 --- /dev/null +++ b/greyjack/greyjack/score_calculation/greynet/function/supplier.py @@ -0,0 +1,9 @@ + +# greynet/function/supplier.py + +from abc import ABC, abstractmethod + +class Supplier(ABC): + @abstractmethod + def get(self): + pass \ No newline at end of file diff --git a/greyjack/greyjack/score_calculation/greynet/function/tri_function.py b/greyjack/greyjack/score_calculation/greynet/function/tri_function.py new file mode 100644 index 0000000..90fdade --- /dev/null +++ b/greyjack/greyjack/score_calculation/greynet/function/tri_function.py @@ -0,0 +1,7 @@ +# greynet/function/tri_function.py +from abc import ABC, abstractmethod + +class TriFunction(ABC): + @abstractmethod + def apply(self, a, b, c): + pass \ No newline at end of file diff --git a/greyjack/greyjack/score_calculation/greynet/function/tri_predicate.py b/greyjack/greyjack/score_calculation/greynet/function/tri_predicate.py new file mode 100644 index 0000000..bf1c51c --- /dev/null +++ b/greyjack/greyjack/score_calculation/greynet/function/tri_predicate.py @@ -0,0 +1,7 @@ +# greynet/function/tri_predicate.py +from abc import ABC, abstractmethod + +class TriPredicate(ABC): + @abstractmethod + def test(self, a, b, c): + pass \ No newline at end of file diff --git a/greyjack/greyjack/score_calculation/greynet/greynet_fact.py b/greyjack/greyjack/score_calculation/greynet/greynet_fact.py new file mode 100644 index 0000000..5b1458e --- /dev/null +++ b/greyjack/greyjack/score_calculation/greynet/greynet_fact.py @@ -0,0 +1,27 @@ +import uuid + +def greynet_fact(cls): + """ + A class decorator that adds an 'greynet_fact_id' attribute to each instance of the class + and overrides the __hash__ method to compute the hash based on the instance-specific greynet_fact_id. + """ + original_init = cls.__init__ + + def __init__(self, *args, **kwargs): + self.greynet_fact_id = uuid.uuid4() + if original_init: + original_init(self, *args, **kwargs) + + def hash_function(self): + return hash(self.greynet_fact_id) + + def eq_function(self, other): + if isinstance(other, cls): + return self.greynet_fact_id == other.greynet_fact_id + return False + + cls.__init__ = __init__ + cls.__hash__ = hash_function + cls.__eq__ = eq_function + return cls + diff --git a/greyjack/greyjack/score_calculation/greynet/nodes/__init__.py b/greyjack/greyjack/score_calculation/greynet/nodes/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/greyjack/greyjack/score_calculation/greynet/nodes/abstract_node.py b/greyjack/greyjack/score_calculation/greynet/nodes/abstract_node.py new file mode 100644 index 0000000..7aa1717 --- /dev/null +++ b/greyjack/greyjack/score_calculation/greynet/nodes/abstract_node.py @@ -0,0 +1,40 @@ +# greynet/nodes/abstract_node.py +from abc import ABC, abstractmethod + +class AbstractNode(ABC): + def __init__(self, node_id): + self._node_id = node_id + self.child_nodes = [] + + def __repr__(self) -> str: + """Provides a developer-friendly representation of the node.""" + return f"<{self.__class__.__name__} id={self._node_id}>" + + def add_child_node(self, child_node): + # This check is critical for node sharing. It ensures that a parent node + # doesn't have duplicate pointers to the same shared child node. + if child_node not in self.child_nodes: + self.child_nodes.append(child_node) + + @abstractmethod + def insert(self, tuple_): + """Processes the insertion of a single tuple.""" + pass + + @abstractmethod + def retract(self, tuple_): + """Processes the retraction of a single tuple.""" + pass + + def calculate_downstream(self, tuples): + """Propagates inserted or updated tuples to all child nodes.""" + for child in self.child_nodes: + for t in tuples: + child.insert(t) + + def retract_downstream(self, tuples): + """Propagates retracted tuples to all child nodes.""" + for child in self.child_nodes: + for t in tuples: + child.retract(t) + diff --git a/greyjack/greyjack/score_calculation/greynet/nodes/alpha_node.py b/greyjack/greyjack/score_calculation/greynet/nodes/alpha_node.py new file mode 100644 index 0000000..39fd9bc --- /dev/null +++ b/greyjack/greyjack/score_calculation/greynet/nodes/alpha_node.py @@ -0,0 +1,34 @@ +# greynet/nodes/alpha_node.py +from ..nodes.abstract_node import AbstractNode + +class AlphaNode(AbstractNode): + """ + An AlphaNode represents a single, intra-element condition. + It filters tuples based on a predicate applied to a single fact. + This is the primary component of the Alpha Network. + """ + def __init__(self, node_id, predicate, scheduler): + super().__init__(node_id) + self.predicate = predicate + self.scheduler = scheduler + # Alpha nodes do not need their own memory; they are simple pass-through filters. + # The first BetaNode they connect to will serve as the memory. + + def insert(self, tuple_): + """ + If the tuple's fact satisfies the predicate, it is propagated + to all child nodes. + """ + # The predicate operates on the fact contained within the tuple. + if self.predicate(tuple_.fact_a): + self.calculate_downstream([tuple_]) + + def retract(self, tuple_): + """ + If the tuple's fact satisfied the predicate, its retraction is + propagated to all child nodes. + """ + # The predicate must be re-evaluated to ensure symmetric retraction. + if self.predicate(tuple_.fact_a): + self.retract_downstream([tuple_]) + diff --git a/greyjack/greyjack/score_calculation/greynet/nodes/base_join_node.py b/greyjack/greyjack/score_calculation/greynet/nodes/base_join_node.py new file mode 100644 index 0000000..2c43d7e --- /dev/null +++ b/greyjack/greyjack/score_calculation/greynet/nodes/base_join_node.py @@ -0,0 +1,100 @@ +# greynet/nodes/base_join_node.py +from __future__ import annotations +from abc import ABC, abstractmethod + +from greyjack.score_calculation.greynet.nodes.abstract_node import AbstractNode +from greyjack.score_calculation.greynet.common.index.uni_index import UniIndex +from greyjack.score_calculation.greynet.common.index.advanced_index import AdvancedIndex +from greyjack.score_calculation.greynet.core.tuple import TupleState, AbstractTuple +from greyjack.score_calculation.greynet.common.joiner_type import JoinerType + +class BaseJoinNode(AbstractNode, ABC): + """ + An abstract base class for all join nodes (Bi, Tri, Quad, etc.). + It contains the common logic for indexing, matching, and propagating tuples, + following the DRY principle. Subclasses only need to implement the + creation of the specific child tuple. + """ + def __init__(self, node_id, joiner_type, left_index_properties, right_index_properties, scheduler, tuple_pool): + super().__init__(node_id) + self.scheduler = scheduler + self.tuple_pair_map = {} + self.left_index = self._create_index(left_index_properties, joiner_type) + self.right_index = self._create_index(right_index_properties, joiner_type) + self.tuple_pool = tuple_pool + + def _create_index(self, props, joiner): + """Factory method to create the appropriate index based on joiner type.""" + if joiner == JoinerType.EQUAL: + return UniIndex(props) + return AdvancedIndex(props, joiner) + + # --- Abstract Method --- + @abstractmethod + def _create_child_tuple(self, left_tuple: AbstractTuple, right_tuple: AbstractTuple) -> AbstractTuple: + """ + Abstract method to be implemented by subclasses. + It must create and return the correct type of child tuple (e.g., BiTuple, TriTuple). + """ + pass + + # --- Common Insertion Logic --- + def insert_left(self, left_tuple: AbstractTuple): + """Handles insertion from the left source stream.""" + self.left_index.put(left_tuple) + key = self.left_index._index_properties.get_property(left_tuple) + right_matches = self.right_index.get_matches(key) if hasattr(self.right_index, 'get_matches') else self.right_index.get(key) + for right_tuple in right_matches: + self.create_and_schedule_child(left_tuple, right_tuple) + + def insert_right(self, right_tuple: AbstractTuple): + """Handles insertion from the right source stream.""" + self.right_index.put(right_tuple) + key = self.right_index._index_properties.get_property(right_tuple) + left_matches = self.left_index.get_matches(key) if hasattr(self.left_index, 'get_matches') else self.left_index.get(key) + for left_tuple in left_matches: + self.create_and_schedule_child(left_tuple, right_tuple) + + # --- Common Retraction Logic --- + def retract_left(self, left_tuple: AbstractTuple): + """Handles retraction from the left source stream.""" + self.left_index.remove(left_tuple) + # Find all pairs involving the retracted left tuple + pairs_to_remove = [p for p in self.tuple_pair_map if p[0] == left_tuple] + for pair in pairs_to_remove: + self.retract_and_schedule_child(pair[0], pair[1]) + + def retract_right(self, right_tuple: AbstractTuple): + """Handles retraction from the right source stream.""" + self.right_index.remove(right_tuple) + # Find all pairs involving the retracted right tuple + pairs_to_remove = [p for p in self.tuple_pair_map if p[1] == right_tuple] + for pair in pairs_to_remove: + self.retract_and_schedule_child(pair[0], pair[1]) + + # --- Common Child Tuple Management --- + def create_and_schedule_child(self, left_tuple: AbstractTuple, right_tuple: AbstractTuple): + """Creates a child tuple using the abstract factory method and schedules it.""" + child = self._create_child_tuple(left_tuple, right_tuple) + child.node, child.state = self, TupleState.CREATING + self.tuple_pair_map[(left_tuple, right_tuple)] = child + self.scheduler.schedule(child) + + def retract_and_schedule_child(self, left: AbstractTuple, right: AbstractTuple): + """Removes a child tuple from the map and schedules its retraction.""" + child = self.tuple_pair_map.pop((left, right), None) + if child: + if child.state == TupleState.CREATING: + child.state = TupleState.ABORTING # Abort if not yet processed + elif not child.state.is_dirty(): + child.state = TupleState.DYING # Schedule for retraction + self.scheduler.schedule(child) + + # --- AbstractNode Contract Fulfillment --- + def insert(self, tuple_): + """Directional inserts (insert_left/right) must be used via adapters.""" + raise NotImplementedError("BaseJoinNode requires directional insert.") + + def retract(self, tuple_): + """Directional retractions (retract_left/right) must be used via adapters.""" + raise NotImplementedError("BaseJoinNode requires directional retract.") diff --git a/greyjack/greyjack/score_calculation/greynet/nodes/beta_node.py b/greyjack/greyjack/score_calculation/greynet/nodes/beta_node.py new file mode 100644 index 0000000..4e76dc9 --- /dev/null +++ b/greyjack/greyjack/score_calculation/greynet/nodes/beta_node.py @@ -0,0 +1,87 @@ +from __future__ import annotations +from abc import ABC, abstractmethod + +from ..nodes.abstract_node import AbstractNode +from ..common.index.uni_index import UniIndex +from ..common.index.advanced_index import AdvancedIndex +from ..core.tuple import TupleState, AbstractTuple +from ..common.joiner_type import JoinerType + +class BetaNode(AbstractNode, ABC): + """ + An abstract base class for all join nodes (Bi, Tri, Quad, etc.). + It contains the common logic for indexing, matching, and propagating tuples. + """ + def __init__(self, node_id, joiner_type, left_index_properties, right_index_properties, scheduler, tuple_pool): + super().__init__(node_id) + self.scheduler = scheduler + self.tuple_pool = tuple_pool + self.joiner_type = joiner_type + self.left_index = self._create_index(left_index_properties, joiner_type) + self.right_index = self._create_index(right_index_properties, joiner_type) + + self.beta_memory = {} + + def __repr__(self) -> str: + """Overrides base representation to include the joiner type.""" + return f"<{self.__class__.__name__} id={self._node_id} joiner={self.joiner_type.name}>" + + + def _create_index(self, props, joiner): + """Factory method to create the appropriate index based on joiner type.""" + if joiner == JoinerType.EQUAL: + return UniIndex(props) + return AdvancedIndex(props, joiner) + + @abstractmethod + def _create_child_tuple(self, left_tuple: AbstractTuple, right_tuple: AbstractTuple) -> AbstractTuple: + """Abstract method to be implemented by subclasses to create the correct child tuple type.""" + pass + + def insert_left(self, left_tuple: AbstractTuple): + self.left_index.put(left_tuple) + key = self.left_index._index_properties.get_property(left_tuple) + right_matches = self.right_index.get_matches(key) if hasattr(self.right_index, 'get_matches') else self.right_index.get(key) + for right_tuple in right_matches: + self.create_and_schedule_child(left_tuple, right_tuple) + + def insert_right(self, right_tuple: AbstractTuple): + self.right_index.put(right_tuple) + key = self.right_index._index_properties.get_property(right_tuple) + left_matches = self.left_index.get_matches(key) if hasattr(self.left_index, 'get_matches') else self.left_index.get(key) + for left_tuple in left_matches: + self.create_and_schedule_child(left_tuple, right_tuple) + + def retract_left(self, left_tuple: AbstractTuple): + self.left_index.remove(left_tuple) + pairs_to_remove = [p for p in self.beta_memory if p[0] == left_tuple] + for pair in pairs_to_remove: + self.retract_and_schedule_child(pair[0], pair[1]) + + def retract_right(self, right_tuple: AbstractTuple): + self.right_index.remove(right_tuple) + pairs_to_remove = [p for p in self.beta_memory if p[1] == right_tuple] + for pair in pairs_to_remove: + self.retract_and_schedule_child(pair[0], pair[1]) + + def create_and_schedule_child(self, left_tuple: AbstractTuple, right_tuple: AbstractTuple): + child = self._create_child_tuple(left_tuple, right_tuple) + child.node, child.state = self, TupleState.CREATING + self.beta_memory[(left_tuple, right_tuple)] = child + self.scheduler.schedule(child) + + def retract_and_schedule_child(self, left: AbstractTuple, right: AbstractTuple): + child = self.beta_memory.pop((left, right), None) + if child: + if child.state == TupleState.CREATING: + child.state = TupleState.ABORTING + elif not child.state.is_dirty(): + child.state = TupleState.DYING + self.scheduler.schedule(child) + + def insert(self, tuple_): + raise NotImplementedError("BetaNode requires directional insert via an adapter.") + + def retract(self, tuple_): + raise NotImplementedError("BetaNode requires directional retract via an adapter.") + diff --git a/greyjack/greyjack/score_calculation/greynet/nodes/conditional_node.py b/greyjack/greyjack/score_calculation/greynet/nodes/conditional_node.py new file mode 100644 index 0000000..4009de4 --- /dev/null +++ b/greyjack/greyjack/score_calculation/greynet/nodes/conditional_node.py @@ -0,0 +1,70 @@ +from ..nodes.abstract_node import AbstractNode +from ..common.index.uni_index import UniIndex + +class ConditionalNode(AbstractNode): + def __init__(self, node_id, left_props, right_props, should_exist, scheduler): + super().__init__(node_id) + self.left_properties = left_props + self.right_properties = right_props + self.should_exist = should_exist + self.scheduler = scheduler + self.left_index = UniIndex(left_props) + self.right_index = UniIndex(right_props) + self.tuple_map = {} # Tracks propagated tuples + + def __repr__(self) -> str: + """Overrides base representation to show the condition type.""" + condition = "EXISTS" if self.should_exist else "NOT EXISTS" + return f"<{self.__class__.__name__} id={self._node_id} condition='{condition}'>" + + def insert_left(self, tuple_): + key = self.left_properties.get_property(tuple_) + self.left_index.put(tuple_) + has_matches = bool(self.right_index.get(key)) + if has_matches == self.should_exist: + self.propagate(tuple_) + else: + pass + + def insert_right(self, tuple_): + key = self.right_properties.get_property(tuple_) + was_empty = not self.right_index.get(key) + self.right_index.put(tuple_) + + if was_empty: + if self.should_exist: + for left_tuple in self.left_index.get(key): + self.propagate(left_tuple) + else: + for left_tuple in self.left_index.get(key): + self.retract_propagation(left_tuple) + + def retract_left(self, tuple_): + self.left_index.remove(tuple_) + self.retract_propagation(tuple_) + + def retract_right(self, tuple_): + key = self.right_properties.get_property(tuple_) + self.right_index.remove(tuple_) + is_now_empty = not self.right_index.get(key) + + if is_now_empty: + if self.should_exist: + for left_tuple in self.left_index.get(key): + self.retract_propagation(left_tuple) + else: + for left_tuple in self.left_index.get(key): + self.propagate(left_tuple) + + def propagate(self, tuple_): + if tuple_ not in self.tuple_map: + self.tuple_map[tuple_] = tuple_ + self.calculate_downstream([tuple_]) + + def retract_propagation(self, tuple_): + if tuple_ in self.tuple_map: + del self.tuple_map[tuple_] + self.retract_downstream([tuple_]) + + def insert(self, tuple_): raise NotImplementedError("ConditionalNode requires directional insert.") + def retract(self, tuple_): raise NotImplementedError("ConditionalNode requires directional retract.") diff --git a/greyjack/greyjack/score_calculation/greynet/nodes/filter_node.py b/greyjack/greyjack/score_calculation/greynet/nodes/filter_node.py new file mode 100644 index 0000000..1d0b278 --- /dev/null +++ b/greyjack/greyjack/score_calculation/greynet/nodes/filter_node.py @@ -0,0 +1,23 @@ +from __future__ import annotations +from typing import Callable +from .abstract_node import AbstractNode +from ..core.tuple import AbstractTuple + +class FilterNode(AbstractNode): + """A generic node that filters tuples of any arity based on a predicate.""" + def __init__(self, node_id: int, predicate: Callable[[AbstractTuple], bool], scheduler): + super().__init__(node_id) + self.predicate = predicate + self.scheduler = scheduler + + def insert(self, tuple_: AbstractTuple): + """If the tuple satisfies the predicate, it is propagated.""" + if self.predicate(tuple_): + self.calculate_downstream([tuple_]) + else: + pass + + def retract(self, tuple_: AbstractTuple): + """If the tuple satisfied the predicate, its retraction is propagated.""" + if self.predicate(tuple_): + self.retract_downstream([tuple_]) diff --git a/greyjack/greyjack/score_calculation/greynet/nodes/flatmap_node.py b/greyjack/greyjack/score_calculation/greynet/nodes/flatmap_node.py new file mode 100644 index 0000000..ebd4117 --- /dev/null +++ b/greyjack/greyjack/score_calculation/greynet/nodes/flatmap_node.py @@ -0,0 +1,47 @@ +from __future__ import annotations +from typing import TYPE_CHECKING +from .abstract_node import AbstractNode +from ..core.tuple import UniTuple, TupleState +from ..function import Function + +if TYPE_CHECKING: + from ..core.tuple_pool import TuplePool + from ..core.tuple import AbstractTuple + +class FlatMapNode(AbstractNode): + def __init__(self, node_id: int, mapper: Function, scheduler, tuple_pool: 'TuplePool'): + super().__init__(node_id) + self.mapper = mapper + self.scheduler = scheduler + self.tuple_pool = tuple_pool + self.parent_to_children_map = {} + + def insert(self, tuple_: 'AbstractTuple'): + + generated_items = self.mapper.apply(tuple_) + if not generated_items: + return + + child_tuples = [] + for item in generated_items: + child_tuple = self.tuple_pool.acquire(UniTuple, fact_a=item) + child_tuple.node = self + child_tuple.state = TupleState.CREATING + self.scheduler.schedule(child_tuple) + child_tuples.append(child_tuple) + + if child_tuples: + self.parent_to_children_map[tuple_] = child_tuples + + def retract(self, tuple_: 'AbstractTuple'): + + child_tuples = self.parent_to_children_map.pop(tuple_, []) + if not child_tuples: + return + + for child in child_tuples: + if child.state == TupleState.CREATING: + child.state = TupleState.ABORTING + elif not child.state.is_dirty(): + child.state = TupleState.DYING + self.scheduler.schedule(child) diff --git a/greyjack/greyjack/score_calculation/greynet/nodes/from_uni_node.py b/greyjack/greyjack/score_calculation/greynet/nodes/from_uni_node.py new file mode 100644 index 0000000..64ebe34 --- /dev/null +++ b/greyjack/greyjack/score_calculation/greynet/nodes/from_uni_node.py @@ -0,0 +1,29 @@ +from ..nodes.abstract_node import AbstractNode +from ..core.tuple import UniTuple, TupleState + +class FromUniNode(AbstractNode): + def __init__(self, node_id, retrieval_id, scheduler, tuple_pool): + super().__init__(node_id) + self.retrieval_id = retrieval_id + self.scheduler = scheduler + self.tuple_pool = tuple_pool + + def __repr__(self) -> str: + """Overrides base representation to show the source fact class.""" + return f"<{self.__class__.__name__} id={self._node_id} fact_class={self.retrieval_id.__name__}>" + + + def insert(self, fact): + # Use the pool to acquire a tuple instead of direct instantiation. + tuple_ = self.tuple_pool.acquire(UniTuple, fact_a=fact) + tuple_.node = self + tuple_.state = TupleState.CREATING + self.scheduler.schedule(tuple_) + return tuple_ + + def retract(self, tuple_): + if tuple_.state == TupleState.CREATING: + tuple_.state = TupleState.ABORTING + elif not tuple_.state.is_dirty(): + tuple_.state = TupleState.DYING + self.scheduler.schedule(tuple_) diff --git a/greyjack/greyjack/score_calculation/greynet/nodes/group_node.py b/greyjack/greyjack/score_calculation/greynet/nodes/group_node.py new file mode 100644 index 0000000..8ddd39a --- /dev/null +++ b/greyjack/greyjack/score_calculation/greynet/nodes/group_node.py @@ -0,0 +1,65 @@ +from ..nodes.abstract_node import AbstractNode +from ..core.tuple import BiTuple, TupleState + +class GroupNode(AbstractNode): + def __init__(self, node_id, group_key_function, collector_supplier, scheduler, tuple_pool): + super().__init__(node_id) + self.group_key_function = group_key_function + self.collector_supplier = collector_supplier + self.scheduler = scheduler + self.group_map = {} + self.tuple_to_undo = {} + self.group_key_to_tuple = {} + self.tuple_pool = tuple_pool + + def insert(self, tuple_): + fact = tuple_.fact_a + group_key = self.group_key_function(fact) + + if group_key not in self.group_map: + self.group_map[group_key] = self.collector_supplier() + + collector = self.group_map[group_key] + undo_function = collector.insert(fact) + self.tuple_to_undo[tuple_] = (group_key, undo_function) + + self._update_or_create_child(group_key, collector) + + def retract(self, tuple_): + if tuple_ not in self.tuple_to_undo: return + group_key, undo_function = self.tuple_to_undo.pop(tuple_) + undo_function() + + collector = self.group_map.get(group_key) + if collector: + if collector.is_empty(): + self._retract_child_by_key(group_key) + del self.group_map[group_key] + else: + self._update_or_create_child(group_key, collector) + + def _update_or_create_child(self, group_key, collector): + child_tuple = self.group_key_to_tuple.get(group_key) + new_result = collector.result() + + if child_tuple: + if child_tuple.fact_b == new_result: + return + self._retract_child_by_key(group_key) + + self._create_child(group_key, new_result) + + def _create_child(self, key, result): + tuple_ = self.tuple_pool.acquire(BiTuple, fact_a=key, fact_b=result) + tuple_.node, tuple_.state = self, TupleState.CREATING + self.group_key_to_tuple[key] = tuple_ + self.scheduler.schedule(tuple_) + + def _retract_child_by_key(self, key): + if key in self.group_key_to_tuple: + tuple_ = self.group_key_to_tuple.pop(key) + if tuple_.state == TupleState.CREATING: + tuple_.state = TupleState.ABORTING + elif not tuple_.state.is_dirty(): + tuple_.state = TupleState.DYING + self.scheduler.schedule(tuple_) diff --git a/greyjack/greyjack/score_calculation/greynet/nodes/join_node.py b/greyjack/greyjack/score_calculation/greynet/nodes/join_node.py new file mode 100644 index 0000000..944ffce --- /dev/null +++ b/greyjack/greyjack/score_calculation/greynet/nodes/join_node.py @@ -0,0 +1,25 @@ +from __future__ import annotations +from typing import Type +from .beta_node import BetaNode +from ..core.tuple import AbstractTuple +from ..tuple_tools import get_facts, get_arity + +class JoinNode(BetaNode): + """A generic beta node that joins two streams and creates a combined tuple.""" + def __init__(self, node_id, joiner_type, left_props, right_props, scheduler, tuple_pool, child_tuple_class: Type[AbstractTuple]): + super().__init__(node_id, joiner_type, left_props, right_props, scheduler, tuple_pool) + self.child_tuple_class = child_tuple_class + # Pre-calculate the attribute names for the child tuple for performance + self.child_fact_names = [f'fact_{chr(97+i)}' for i in range(get_arity(child_tuple_class))] + + def _create_child_tuple(self, left_tuple: AbstractTuple, right_tuple: AbstractTuple) -> AbstractTuple: + """Creates a new tuple by combining the facts from the parent tuples.""" + combined_facts = get_facts(left_tuple) + get_facts(right_tuple) + + # Create a kwargs dictionary like {'fact_a': v1, 'fact_b': v2, ...} + fact_kwargs = dict(zip(self.child_fact_names, combined_facts)) + + return self.tuple_pool.acquire( + self.child_tuple_class, + **fact_kwargs + ) diff --git a/greyjack/greyjack/score_calculation/greynet/nodes/scoring_node.py b/greyjack/greyjack/score_calculation/greynet/nodes/scoring_node.py new file mode 100644 index 0000000..66b3195 --- /dev/null +++ b/greyjack/greyjack/score_calculation/greynet/nodes/scoring_node.py @@ -0,0 +1,60 @@ +from ..nodes.abstract_node import AbstractNode +class ScoringNode(AbstractNode): + def __init__(self, node_id, constraint_id, impact_function, score_class): + super().__init__(node_id) + self.constraint_id = constraint_id + self.impact_function = impact_function + self.score_class = score_class + self.matches = {} + + def __repr__(self) -> str: + """Overrides base representation to show the constraint ID.""" + return f"<{self.__class__.__name__} id={self._node_id} constraint_id='{self.constraint_id}'>" + + + def insert(self, tuple_): + args = [f for f in ( + getattr(tuple_, 'fact_a', None), + getattr(tuple_, 'fact_b', None), + getattr(tuple_, 'fact_c', None), + getattr(tuple_, 'fact_d', None), + getattr(tuple_, 'fact_e', None) + ) if f is not None] + + score_object = self.impact_function(*args) + + if not isinstance(score_object, self.score_class): + raise TypeError( + f"Impact function for constraint '{self.constraint_id}' " + f"returned type {type(score_object).__name__}, but session " + f"is configured for {self.score_class.__name__}." + ) + + match = (score_object, tuple_) + self.matches[tuple_] = match + + def retract(self, tuple_): + if tuple_ in self.matches: + del self.matches[tuple_] + + def recalculate_scores(self): + """ + Iterates over all existing matches and re-calculates their score. + """ + for tuple_ in list(self.matches.keys()): + args = [f for f in ( + getattr(tuple_, 'fact_a', None), + getattr(tuple_, 'fact_b', None), + getattr(tuple_, 'fact_c', None), + getattr(tuple_, 'fact_d', None), + getattr(tuple_, 'fact_e', None) + ) if f is not None] + + new_score_object = self.impact_function(*args) + self.matches[tuple_] = (new_score_object, tuple_) + + def get_total_score(self): + total = self.score_class.get_null_score() + for score, _ in self.matches.values(): + total += score + return total diff --git a/greyjack/greyjack/score_calculation/greynet/nodes/sequence_pattern_node.py b/greyjack/greyjack/score_calculation/greynet/nodes/sequence_pattern_node.py new file mode 100644 index 0000000..d948601 --- /dev/null +++ b/greyjack/greyjack/score_calculation/greynet/nodes/sequence_pattern_node.py @@ -0,0 +1,164 @@ + +# greynet/nodes/sequence_pattern_node.py + +from __future__ import annotations +import bisect +from collections import defaultdict +from datetime import datetime +from typing import Callable, Any, List + +from ..nodes.abstract_node import AbstractNode +from ..core.tuple import UniTuple, TupleState, AbstractTuple +from ..core.tuple_pool import TuplePool +from ..collectors.temporal_collectors import EventSequencePattern + +class SequencePatternNode(AbstractNode): + """ + A node that correctly detects sequences of facts matching a defined pattern. + + This version contains a corrected algorithm for sequence detection that properly + handles event insertion and retraction by rescanning for all valid sequences + and reconciling the engine's state. + """ + def __init__(self, node_id: int, pattern: EventSequencePattern, + time_extractor: Callable[[Any], datetime], scheduler, tuple_pool: TuplePool): + super().__init__(node_id) + self._pattern = pattern + self._time_extractor = time_extractor + self._scheduler = scheduler + self._tuple_pool = tuple_pool + + # State Management + # A sorted list of all events (facts) that could be part of a sequence. + self._events: List[tuple[datetime, AbstractTuple]] = [] + # Tracks active sequences to manage propagation and retraction. + # Key: frozenset of parent tuple IDs. Value: The propagated child tuple. + self._active_sequences: dict[frozenset[int], UniTuple] = {} + + def insert(self, parent_tuple: AbstractTuple): + """Inserts a fact, adds it to the event timeline, and re-evaluates sequences.""" + timestamp = self._time_extractor(parent_tuple.fact_a) + event_entry = (timestamp, parent_tuple) + + # Insert event while maintaining chronological order. + bisect.insort_left(self._events, event_entry) + self._rescan_and_reconcile() + + def retract(self, parent_tuple: AbstractTuple): + """Retracts a fact, removes it from the timeline, and re-evaluates sequences.""" + timestamp = self._time_extractor(parent_tuple.fact_a) + event_entry = (timestamp, parent_tuple) + + try: + self._events.remove(event_entry) + self._rescan_and_reconcile() + except ValueError: + # Fact was not in the event list, nothing to do. + return + + def _rescan_and_reconcile(self): + """ + Scans for all currently valid sequences and reconciles the engine state + by propagating new sequences and retracting those that are no longer valid. + """ + all_found_sequences = self._find_all_valid_sequences() + + current_keys = set(self._active_sequences.keys()) + found_keys = set(all_found_sequences.keys()) + + # Retract sequences that were active but are no longer valid. + for key in current_keys - found_keys: + child_tuple = self._active_sequences.pop(key) + self._retract_child(child_tuple) + + # Propagate new valid sequences that were not previously active. + for key in found_keys - current_keys: + parent_tuples = all_found_sequences[key] + child_tuple = self._propagate_new_sequence(parent_tuples) + self._active_sequences[key] = child_tuple + + def _find_all_valid_sequences(self) -> dict[frozenset[int], List[AbstractTuple]]: + """ + Finds all unique, complete sequences that match the pattern from the current event list. + """ + found_sequences = {} + + # Iterate through all events, treating each as a potential start of a sequence. + for i, (start_time, start_tuple) in enumerate(self._events): + # The event must match the first step of the pattern. + if self._pattern.pattern_steps[0](start_tuple.fact_a): + # Find all possible complete sequences starting from this event. + initial_sequence = [start_tuple] + complete_sequences = self._find_sequence_completions( + current_sequence=initial_sequence, + search_from_index=i + 1, + sequence_start_time=start_time + ) + + for sequence in complete_sequences: + # Use a key based on the object IDs of the facts to uniquely + # identify this specific instance of a sequence. + key = frozenset(t.greynet_fact_id for t in sequence) + if key not in found_sequences: + found_sequences[key] = sequence + + return found_sequences + + def _find_sequence_completions(self, current_sequence: List[AbstractTuple], + search_from_index: int, + sequence_start_time: datetime) -> List[List[AbstractTuple]]: + """ + A robust recursive function to find all valid completions of a partial sequence. + """ + # Base case: The sequence has the required number of steps. It's a valid completion. + if len(current_sequence) == len(self._pattern.pattern_steps): + return [current_sequence] + + next_step_index = len(current_sequence) + next_predicate = self._pattern.pattern_steps[next_step_index] + all_completions = [] + + # Iterate through subsequent events to find a match for the next step. + for i in range(search_from_index, len(self._events)): + event_time, event_tuple = self._events[i] + + # Optimization: Stop searching if the event is outside the pattern's time window. + if event_time - sequence_start_time > self._pattern.within: + break + + # Check if this event matches the predicate for the next step in the pattern. + if next_predicate(event_tuple.fact_a): + # We found a potential next event. + extended_sequence = current_sequence + [event_tuple] + + # Recursively find the rest of the sequence, starting from the event AFTER this one. + completions = self._find_sequence_completions( + extended_sequence, + i + 1, + sequence_start_time + ) + all_completions.extend(completions) + + # If gaps are not allowed, we MUST use this first match we found. + # We cannot continue searching for other candidates for this same step. + if not self._pattern.allow_gaps: + break + + return all_completions + + def _propagate_new_sequence(self, parent_tuples: List[AbstractTuple]) -> UniTuple: + """Creates and schedules a new child tuple representing a valid sequence.""" + # The fact of the child tuple is the list of facts from the sequence. + sequence_facts = [p.fact_a for p in parent_tuples] + child_tuple = self._tuple_pool.acquire(UniTuple, fact_a=sequence_facts) + child_tuple.node, child_tuple.state = self, TupleState.CREATING + self._scheduler.schedule(child_tuple) + return child_tuple + + def _retract_child(self, child_tuple: UniTuple): + """Schedules a child tuple for retraction if it's no longer valid.""" + if child_tuple.state == TupleState.CREATING: + child_tuple.state = TupleState.ABORTING + elif not child_tuple.state.is_dirty(): + child_tuple.state = TupleState.DYING + self._scheduler.schedule(child_tuple) diff --git a/greyjack/greyjack/score_calculation/greynet/nodes/sliding_window_node.py b/greyjack/greyjack/score_calculation/greynet/nodes/sliding_window_node.py new file mode 100644 index 0000000..c71c556 --- /dev/null +++ b/greyjack/greyjack/score_calculation/greynet/nodes/sliding_window_node.py @@ -0,0 +1,120 @@ +# greynet/nodes/sliding_window_node.py + +from __future__ import annotations +import bisect +from collections import defaultdict +from datetime import datetime, timedelta, timezone + +from .abstract_node import AbstractNode +from ..core.tuple import BiTuple, TupleState, AbstractTuple +from ..core.tuple_pool import TuplePool + +class SlidingWindowNode(AbstractNode): + """ + A node that groups facts into sliding time windows. + + For each window, it emits a BiTuple where: + - fact_a: The start datetime of the window. + - fact_b: A list of all facts that fall within that window. + + The node correctly handles insertion and retraction of facts, updating + the corresponding window tuples as needed. + """ + def __init__(self, node_id: int, time_extractor: callable, window_size: timedelta, + slide_interval: timedelta, scheduler, tuple_pool: TuplePool): + super().__init__(node_id) + self._time_extractor = time_extractor + self._window_size = window_size + self._slide_interval = slide_interval + self._scheduler = scheduler + self._tuple_pool = tuple_pool + + # State management + self._events: list[tuple[datetime, AbstractTuple]] = [] + self._fact_to_windows = defaultdict(set) # Maps fact.id -> {window_start_ts, ...} + self._active_windows = {} # Maps window_start_ts -> emitted_BiTuple + + # Use a fixed epoch for reproducible window alignment + self._epoch_start_ts = datetime(1970, 1, 1, tzinfo=timezone.utc).timestamp() + self._slide_sec = self._slide_interval.total_seconds() + self._window_sec = self._window_size.total_seconds() + + def _get_windows_for_timestamp(self, ts: datetime) -> list[datetime]: + """Calculates all sliding windows a given timestamp falls into.""" + ts_seconds = ts.timestamp() + # First possible window starts such that its end (start + size) includes the timestamp + first_window_start_idx = (ts_seconds - self._window_sec) / self._slide_sec + # Last possible window starts such that its start is before or at the timestamp + last_window_start_idx = ts_seconds / self._slide_sec + + windows = [] + # Iterate through all possible window start indices + for i in range(int(first_window_start_idx) + 1, int(last_window_start_idx) + 1): + start_ts = self._epoch_start_ts + i * self._slide_sec + windows.append(datetime.fromtimestamp(start_ts, tz=timezone.utc)) + return windows + + def insert(self, tuple_): + fact = tuple_.fact_a + fact_id = fact.id + timestamp = self._time_extractor(fact) + + # Maintain a sorted list of events + event_entry = (timestamp, fact) + bisect.insort(self._events, event_entry) + + affected_windows = self._get_windows_for_timestamp(timestamp) + for window_start in affected_windows: + self._fact_to_windows[fact_id].add(window_start) + self._update_window(window_start) + + def retract(self, tuple_): + fact = tuple_.fact_a + fact_id = fact.id + timestamp = self._time_extractor(fact) + + event_entry = (timestamp, fact) + try: + # O(N) removal, can be optimized if needed + self._events.remove(event_entry) + except ValueError: + return # Fact was not present + + # Update all windows this fact was part of + if fact_id in self._fact_to_windows: + affected_windows = self._fact_to_windows.pop(fact_id) + for window_start in affected_windows: + self._update_window(window_start) + + def _update_window(self, window_start: datetime): + """(Re)calculates and propagates a single window.""" + window_end = window_start + self._window_size + + # Find all facts within this window's time range from the sorted list + start_idx = bisect.bisect_left(self._events, (window_start, None)) + end_idx = bisect.bisect_right(self._events, (window_end, None)) + + facts_in_window = [fact for ts, fact in self._events[start_idx:end_idx]] + + # Retract the old tuple for this window if it exists + if window_start in self._active_windows: + old_tuple = self._active_windows.pop(window_start) + self._retract_child(old_tuple) + + # If there are facts, create and propagate a new tuple + if facts_in_window: + new_tuple = self._create_child(window_start, facts_in_window) + self._active_windows[window_start] = new_tuple + + def _create_child(self, key: datetime, result: list) -> BiTuple: + tuple_ = self._tuple_pool.acquire(BiTuple, fact_a=key, fact_b=result) + tuple_.node, tuple_.state = self, TupleState.CREATING + self._scheduler.schedule(tuple_) + return tuple_ + + def _retract_child(self, tuple_: BiTuple): + if tuple_.state == TupleState.CREATING: + tuple_.state = TupleState.ABORTING + elif not tuple_.state.is_dirty(): + tuple_.state = TupleState.DYING + self._scheduler.schedule(tuple_) diff --git a/greyjack/greyjack/score_calculation/greynet/optimization/__init__.py b/greyjack/greyjack/score_calculation/greynet/optimization/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/greyjack/greyjack/score_calculation/greynet/optimization/batch_processor.py b/greyjack/greyjack/score_calculation/greynet/optimization/batch_processor.py new file mode 100644 index 0000000..b0befb2 --- /dev/null +++ b/greyjack/greyjack/score_calculation/greynet/optimization/batch_processor.py @@ -0,0 +1,39 @@ +from collections import deque +from ..core.scheduler import Scheduler +from ..core.tuple import TupleState +from ..core.tuple_pool import TuplePool + + +class BatchScheduler(Scheduler): + def __init__(self, node_map, tuple_pool: TuplePool, batch_size=100): + super().__init__(node_map) + self.batch_size = batch_size + self.pending_queue = deque() + self.tuple_pool = tuple_pool + + def schedule(self, tuple_): + self.pending_queue.append(tuple_) + + def fire_all(self): + """Processes all pending tuple changes in the queue.""" + while self.pending_queue: + tuple_ = self.pending_queue.popleft() + node = tuple_.node + state = tuple_.state + + if state == TupleState.CREATING: + node.calculate_downstream([tuple_]) + tuple_.state = TupleState.OK + elif state == TupleState.UPDATING: + node.retract_downstream([tuple_]) + node.calculate_downstream([tuple_]) + tuple_.state = TupleState.OK + elif state == TupleState.DYING: + node.retract_downstream([tuple_]) + tuple_.state = TupleState.DEAD + elif state == TupleState.ABORTING: + tuple_.state = TupleState.DEAD + + # If the tuple is now dead, release it back to the pool. + if tuple_.state == TupleState.DEAD: + self.tuple_pool.release(tuple_) diff --git a/greyjack/greyjack/score_calculation/greynet/optimization/node_sharing.py b/greyjack/greyjack/score_calculation/greynet/optimization/node_sharing.py new file mode 100644 index 0000000..4b6903f --- /dev/null +++ b/greyjack/greyjack/score_calculation/greynet/optimization/node_sharing.py @@ -0,0 +1,41 @@ +# greynet/optimization/node_sharing.py +from __future__ import annotations +from typing import Dict, Any + +class NodeSharingManager: + """ + Manages the creation and sharing of nodes within the Rete network. + + This class holds maps for different types of nodes (alpha, beta, etc.) + and ensures that if a node with an identical definition is requested, + the existing instance is returned instead of creating a new one. + This is the core of the node sharing optimization. + """ + def __init__(self): + # A map for each category of shareable node. + # The key is the stream's unique `retrieval_id`. + # The value is the created node instance. + self.alpha_nodes: Dict[Any, Any] = {} + self.beta_nodes: Dict[Any, Any] = {} + self.group_nodes: Dict[Any, Any] = {} + self.temporal_nodes: Dict[Any, Any] = {} + # Other node types like 'from' or 'scoring' are typically not shared + # as they are entry/exit points for a specific rule or fact type. + + def get_or_create_node(self, retrieval_id: Any, node_map: Dict, node_supplier: callable): + """ + Generic factory method to get an existing node or create a new one. + + Args: + retrieval_id: The unique identifier for the stream/node definition. + node_map (Dict): The specific dictionary to check (e.g., self.alpha_nodes). + node_supplier (callable): A function that creates and returns a new node instance. + + Returns: + A shared node instance. + """ + node = node_map.get(retrieval_id) + if node is None: + node = node_supplier() + node_map[retrieval_id] = node + return node diff --git a/greyjack/greyjack/score_calculation/greynet/session.py b/greyjack/greyjack/score_calculation/greynet/session.py new file mode 100644 index 0000000..e5da094 --- /dev/null +++ b/greyjack/greyjack/score_calculation/greynet/session.py @@ -0,0 +1,122 @@ +from __future__ import annotations + +from .core.tuple_pool import TuplePool + +class Session: + def __init__(self, from_nodes, scoring_nodes, scheduler, score_class, tuple_pool: TuplePool, weights=None): + self.from_nodes = from_nodes + self.scoring_nodes = scoring_nodes + self.scheduler = scheduler + self.score_class = score_class + self.fact_id_to_tuple = {} + self.tuple_pool = tuple_pool + self.weights = weights + self._scoring_node_map = {node.constraint_id: node for node in scoring_nodes} + + def insert(self, fact): + """Inserts a single fact and immediately processes the consequences.""" + self.insert_batch([fact]) + self.flush() + + def retract(self, fact): + """Retracts a single fact and immediately processes the consequences.""" + self.retract_batch([fact]) + self.flush() + + def insert_batch(self, facts): + """Inserts a collection of facts into the network.""" + + for fact in facts: + fact_type = type(fact) + if fact_type not in self.from_nodes: + continue + + fact_id = fact.greynet_fact_id + if fact_id in self.fact_id_to_tuple: + continue + + from_node = self.from_nodes[fact_type] + tuple_ = from_node.insert(fact) + self.fact_id_to_tuple[fact_id] = tuple_ + + def retract_batch(self, facts): + """Retracts a collection of facts from the network.""" + + for fact in facts: + fact_id = fact.greynet_fact_id + tuple_ = self.fact_id_to_tuple.pop(fact_id, None) + if tuple_ is None: + continue + + tuple_.node.retract(tuple_) + + def flush(self): + """Processes all pending changes in the scheduler queue.""" + self.scheduler.fire_all() + + def clear(self): + """ + Retracts all known facts from the session and flushes the network. + This effectively resets the session's state and releases all tuple + objects back to the pool. + """ + all_tuples = list(self.fact_id_to_tuple.values()) + + for tuple_ in all_tuples: + tuple_.node.retract(tuple_) + + self.fact_id_to_tuple.clear() + + self.flush() + + def update_constraint_weight(self, constraint_id: str, new_weight: float): + """ + Updates the weight for a constraint and triggers an immediate recalculation + of scores for all existing matches of that constraint. + """ + if self.weights is None: + raise RuntimeError("Cannot update weights. Session was not initialized with a weights manager.") + if constraint_id not in self._scoring_node_map: + raise ValueError(f"No constraint found with ID: '{constraint_id}'") + + self.weights.set_weight(constraint_id, new_weight) + + scoring_node = self._scoring_node_map[constraint_id] + + scoring_node.recalculate_scores() + + def get_score(self): + """ + Flushes all pending changes and calculates the total score. + Returns a score object (e.g., SimpleScore, HardSoftScore). + """ + self.flush() + + total_score = self.score_class.get_null_score() + for node in self.scoring_nodes: + total_score += node.get_total_score() + return total_score + + def get_constraint_matches(self): + """ + Flushes all pending changes and returns a detailed breakdown of + all constraint violations. + """ + self.flush() + matches = {} + for node in self.scoring_nodes: + if node.matches: + matches[node.constraint_id] = list(node.matches.values()) + return matches + + def recalculate_all_scores(self): + """ + Forces a recalculation of all scores for all active constraint matches. + + This is a lightweight recovery method ideal for scenarios where external factors, + like constraint weights, have changed. It does not re-evaluate the network's + joins or filters; it only re-runs the final impact function for each + existing match. + """ + for node in self.scoring_nodes: + node.recalculate_scores() \ No newline at end of file diff --git a/greyjack/greyjack/score_calculation/greynet/streams/__init__.py b/greyjack/greyjack/score_calculation/greynet/streams/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/greyjack/greyjack/score_calculation/greynet/streams/abstract_stream.py b/greyjack/greyjack/score_calculation/greynet/streams/abstract_stream.py new file mode 100644 index 0000000..0afc2a1 --- /dev/null +++ b/greyjack/greyjack/score_calculation/greynet/streams/abstract_stream.py @@ -0,0 +1,49 @@ +from __future__ import annotations +from abc import ABC, abstractmethod +from typing import Callable, Union, TYPE_CHECKING + +from ..constraint import Constraint +from ..common.index_properties import IndexProperties +from ..function import Function + +if TYPE_CHECKING: + from ..constraint_factory import ConstraintFactory + from ..core.tuple_pool import TuplePool + + +class AbstractStream(ABC): + def __init__(self, constraint_factory: 'ConstraintFactory', retrieval_id): + self.constraint_factory = constraint_factory + self.retrieval_id = retrieval_id + self.next_streams = [] + self.source_stream = None + + def add_next_stream(self, stream: AbstractStream): + self.next_streams.append(stream) + + def and_source(self, stream: AbstractStream): + self.source_stream = stream + + def _create_penalty(self, score_type: str, penalty: Union[int, float, Callable]) -> Constraint: + """Helper to package stream and penalty info into a Constraint object.""" + penalty_function = penalty + if not callable(penalty_function): + penalty_function = lambda *facts: penalty + + return Constraint(stream=self, score_type=score_type, penalty_function=penalty_function) + + def penalize_hard(self, penalty: Union[int, float, Callable]) -> Constraint: + return self._create_penalty("hard", penalty) + + def penalize_medium(self, penalty: Union[int, float, Callable]) -> Constraint: + return self._create_penalty("medium", penalty) + + def penalize_soft(self, penalty: Union[int, float, Callable]) -> Constraint: + return self._create_penalty("soft", penalty) + + def penalize_simple(self, penalty: Union[int, float, Callable]) -> Constraint: + return self._create_penalty("simple", penalty) + + @abstractmethod + def build_node(self, node_counter, node_map, scheduler, tuple_pool: TuplePool): + pass diff --git a/greyjack/greyjack/score_calculation/greynet/streams/join_adapters.py b/greyjack/greyjack/score_calculation/greynet/streams/join_adapters.py new file mode 100644 index 0000000..f6c2f9c --- /dev/null +++ b/greyjack/greyjack/score_calculation/greynet/streams/join_adapters.py @@ -0,0 +1,86 @@ +# greynet/streams/join_adapters.py +from __future__ import annotations +from typing import TYPE_CHECKING + +from ..nodes.abstract_node import AbstractNode +from ..core.tuple import AbstractTuple + +if TYPE_CHECKING: + from ..nodes.beta_node import BetaNode + + +class JoinLeftAdapter(AbstractNode): + """ + An adapter that redirects generic insert/retract calls to the 'left' inputs + of a BetaNode (e.g., JoinNode, ConditionalNode). + + This allows a parent node to treat the BetaNode as a simple child, while the + BetaNode can correctly distinguish between its left and right inputs. + """ + def __init__(self, beta_node: 'BetaNode'): + """ + Initializes the adapter. + + Args: + beta_node: The beta node (e.g., JoinNode) to which calls will be redirected. + """ + # Note: We don't call super().__init__() as this adapter does not need a node_id + # and will never have its own children. It is a terminal pass-through. + self.beta_node = beta_node + + def __repr__(self) -> str: + """Provides a representation showing which node it adapts.""" + return f"<{self.__class__.__name__} for_node={self.beta_node!r}>" + + + def insert(self, tuple_: AbstractTuple): + """ + Receives a tuple from the parent (left) stream and passes it to the + beta_node's insert_left method. + """ + self.beta_node.insert_left(tuple_) + + def retract(self, tuple_: AbstractTuple): + """ + Receives a retraction from the parent (left) stream and passes it to the + beta_node's retract_left method. + """ + self.beta_node.retract_left(tuple_) + + +class JoinRightAdapter(AbstractNode): + """ + An adapter that redirects generic insert/retract calls to the 'right' inputs + of a BetaNode (e.g., JoinNode, ConditionalNode). + + This allows a parent node to treat the BetaNode as a simple child, while the + BetaNode can correctly distinguish between its left and right inputs. + """ + def __init__(self, beta_node: 'BetaNode'): + """ + Initializes the adapter. + + Args: + beta_node: The beta node (e.g., JoinNode) to which calls will be redirected. + """ + self.beta_node = beta_node + + def __repr__(self) -> str: + """Provides a representation showing which node it adapts.""" + return f"<{self.__class__.__name__} for_node={self.beta_node!r}>" + + + def insert(self, tuple_: AbstractTuple): + """ + Receives a tuple from the parent (right) stream and passes it to the + beta_node's insert_right method. + """ + self.beta_node.insert_right(tuple_) + + def retract(self, tuple_: AbstractTuple): + """ + Receives a retraction from the parent (right) stream and passes it to the + beta_node's retract_right method. + """ + self.beta_node.retract_right(tuple_) + diff --git a/greyjack/greyjack/score_calculation/greynet/streams/scoring_stream.py b/greyjack/greyjack/score_calculation/greynet/streams/scoring_stream.py new file mode 100644 index 0000000..1187134 --- /dev/null +++ b/greyjack/greyjack/score_calculation/greynet/streams/scoring_stream.py @@ -0,0 +1,24 @@ +from ..streams.abstract_stream import AbstractStream +from ..nodes.scoring_node import ScoringNode + +class ScoringStream(AbstractStream): + def __init__(self, source_stream, constraint_id, impact_function): + retrieval_id = ("score", constraint_id) + super().__init__(source_stream.constraint_factory, retrieval_id) + self.and_source(source_stream) + self.constraint_id = constraint_id + self.impact_function = impact_function + + def build_node(self, node_counter, node_map, scheduler, tuple_pool): + node = node_map.get(self.retrieval_id) + if node is None: + source_node = self.source_stream.build_node(node_counter, node_map, scheduler, tuple_pool) + node_id = node_counter.value + node_counter.value += 1 + + score_class = self.constraint_factory.score_class + node = ScoringNode(node_id, self.constraint_id, self.impact_function, score_class) + + source_node.add_child_node(node) + node_map[self.retrieval_id] = node + return node diff --git a/greyjack/greyjack/score_calculation/greynet/streams/stream.py b/greyjack/greyjack/score_calculation/greynet/streams/stream.py new file mode 100644 index 0000000..b67c07b --- /dev/null +++ b/greyjack/greyjack/score_calculation/greynet/streams/stream.py @@ -0,0 +1,193 @@ +from __future__ import annotations +from typing import Generic, TypeVar, Callable, Type, Union, TYPE_CHECKING, Any, Iterable, List +from datetime import timedelta, datetime + +from ..constraint import Constraint +from ..tuple_tools import get_facts +from .stream_definition import ( + FilterDefinition, JoinDefinition, + GroupByDefinition, ConditionalJoinDefinition, FlatMapDefinition, + SlidingWindowDefinition, SequencePatternDefinition +) +from ..core.tuple import UniTuple, BiTuple, AbstractTuple +from ..collectors.temporal_collectors import EventSequencePattern + +if TYPE_CHECKING: + from ..constraint_factory import ConstraintFactory + from ..core.tuple import AbstractTuple + from ..common.joiner_type import JoinerType + from .stream_definition import StreamDefinition + + +T_Tuple = TypeVar('T_Tuple', bound='AbstractTuple') + +class Stream(Generic[T_Tuple]): + """A generic, unified stream for processing tuples of any arity.""" + + def __init__(self, factory: 'ConstraintFactory', definition: 'StreamDefinition'): + self.constraint_factory = factory + self.definition = definition + self.arity = definition.get_target_arity() + self.next_streams: list[Stream] = [] + + def _add_next_stream(self, stream: 'Stream'): + self.next_streams.append(stream) + + def filter(self, predicate: Callable[..., bool]) -> Stream[T_Tuple]: + """Filters the stream based on a predicate.""" + filter_def = FilterDefinition(self.constraint_factory, self, predicate) + new_stream = Stream[T_Tuple](self.constraint_factory, filter_def) + self._add_next_stream(new_stream) + return new_stream + + def join(self, other_stream: Stream[Any], joiner_type: 'JoinerType', + left_key_func: Callable[..., Any], right_key_func: Callable[..., Any]) -> Stream[AbstractTuple]: + """Joins this stream with another stream.""" + join_def = JoinDefinition( + self.constraint_factory, self, other_stream, + joiner_type, left_key_func, right_key_func + ) + new_stream = Stream[AbstractTuple](self.constraint_factory, join_def) + self._add_next_stream(new_stream) + other_stream._add_next_stream(new_stream) + return new_stream + + def group_by(self, group_key_function: Callable[..., Any], collector_supplier: Callable) -> Stream['BiTuple']: + """Groups tuples by a key and applies a collector, returning a stream of (key, result) BiTuples.""" + group_by_def = GroupByDefinition(self.constraint_factory, self, group_key_function, collector_supplier) + new_stream = Stream[BiTuple](self.constraint_factory, group_by_def) + self._add_next_stream(new_stream) + return new_stream + + def if_exists(self, other_stream: Stream[Any], left_key: Callable[..., Any], + right_key: Callable[..., Any]) -> Stream[T_Tuple]: + """Propagates tuples only if a match exists in the other stream.""" + # Allow passing a fact class directly for convenience + if not isinstance(other_stream, Stream): + other_stream = self.constraint_factory.from_(other_stream) + + cond_def = ConditionalJoinDefinition( + self.constraint_factory, self, self, other_stream, True, + left_key, right_key + ) + new_stream = Stream[T_Tuple](self.constraint_factory, cond_def) + self._add_next_stream(new_stream) + other_stream._add_next_stream(new_stream) + return new_stream + + def if_not_exists(self, other_stream: Stream[Any], left_key: Callable[..., Any], + right_key: Callable[..., Any]) -> Stream[T_Tuple]: + """Propagates tuples only if no match exists in the other stream.""" + # Allow passing a fact class directly for convenience + if not isinstance(other_stream, Stream): + other_stream = self.constraint_factory.from_(other_stream) + + cond_def = ConditionalJoinDefinition( + self.constraint_factory, self, self, other_stream, False, + left_key, right_key + ) + new_stream = Stream[T_Tuple](self.constraint_factory, cond_def) + self._add_next_stream(new_stream) + other_stream._add_next_stream(new_stream) + return new_stream + + def flat_map(self, mapper: Callable[..., Iterable[Any]]) -> Stream['UniTuple']: + """Transforms each element into an iterable of new elements, flattening the result.""" + flat_map_def = FlatMapDefinition(self.constraint_factory, self, mapper) + new_stream = Stream[UniTuple](self.constraint_factory, flat_map_def) + self._add_next_stream(new_stream) + return new_stream + + def map(self, mapper: Callable[..., Any]) -> Stream['UniTuple']: + """Transforms each element into a single new element.""" + wrapped_mapper = lambda *facts: [mapper(*facts)] + return self.flat_map(wrapped_mapper) + + def build_node(self, node_counter, node_map, scheduler, tuple_pool): + """Builds the Rete node for this stream and wires its children.""" + node = self.definition.build_node(node_counter, node_map, scheduler, tuple_pool) + return node + + # --- Temporal and Sequential Extensions --- + + def sequence(self, time_extractor: Callable[[Any], datetime], *steps: Callable[[Any], bool], + within: timedelta, allow_gaps: bool = True) -> Stream['UniTuple']: + """ + Finds sequences of facts that match a series of predicates within a time window. + The stream emits a list of the facts that form a complete sequence. + """ + if self.arity != 1: + raise TypeError("The .sequence() operation can only be applied to a stream of single facts (UniTuple).") + + # --- Start of Bug Fix --- + # `steps` is already a tuple. Using `list(steps)` made the pattern unhashable. + pattern = EventSequencePattern(pattern_steps=steps, within=within, allow_gaps=allow_gaps) + # --- End of Bug Fix --- + seq_def = SequencePatternDefinition(self.constraint_factory, self, pattern, time_extractor) + new_stream = Stream[UniTuple](self.constraint_factory, seq_def) + self._add_next_stream(new_stream) + return new_stream + + def window(self, time_extractor: Callable[[Any], datetime]) -> 'WindowedStream': + """ + Initiates a windowing operation on the stream. Must be followed by a window type + like .sliding() or .tumbling(). + """ + if self.arity != 1: + raise TypeError("Windowing operations can only be applied to a stream of single facts (UniTuple).") + return WindowedStream(self, time_extractor) + + # --- Penalty Methods --- + def _create_penalty(self, score_type: str, penalty: Union[int, float, Callable]) -> Constraint: + penalty_function = penalty + if not callable(penalty_function): + penalty_function = lambda *facts: penalty + + def impact_wrapper(*facts): + return penalty_function(*facts) + + return Constraint(stream=self, score_type=score_type, penalty_function=impact_wrapper) + + def penalize_hard(self, penalty: Union[int, float, Callable]) -> Constraint: + return self._create_penalty("hard", penalty) + + def penalize_soft(self, penalty: Union[int, float, Callable]) -> Constraint: + return self._create_penalty("soft", penalty) + + def penalize_medium(self, penalty: Union[int, float, Callable]) -> Constraint: + return self._create_penalty("medium", penalty) + + def penalize_simple(self, penalty: Union[int, float, Callable]) -> Constraint: + return self._create_penalty("simple", penalty) + + +class WindowedStream: + """A helper class to provide a fluent API for creating time windows.""" + def __init__(self, source_stream: Stream, time_extractor: Callable[[Any], datetime]): + self._source_stream = source_stream + self._time_extractor = time_extractor + + def sliding(self, size: timedelta, slide: timedelta) -> Stream['BiTuple']: + """ + Groups facts into overlapping windows of a fixed size that advance at a specified interval. + Emits a stream of (window_start_time, [facts_in_window]) BiTuples. + """ + if slide.total_seconds() <= 0 or size.total_seconds() <= 0: + raise ValueError("Window size and slide interval must be positive.") + if slide > size: + raise ValueError("Slide interval cannot be greater than the window size for a sliding window.") + + window_def = SlidingWindowDefinition( + self._source_stream.constraint_factory, self._source_stream, + self._time_extractor, size, slide + ) + new_stream = Stream[BiTuple](self._source_stream.constraint_factory, window_def) + self._source_stream._add_next_stream(new_stream) + return new_stream + + def tumbling(self, size: timedelta) -> Stream['BiTuple']: + """ + Groups facts into non-overlapping windows of a fixed size. + This is a special case of a sliding window where the slide interval equals the size. + """ + return self.sliding(size=size, slide=size) diff --git a/greyjack/greyjack/score_calculation/greynet/streams/stream_definition.py b/greyjack/greyjack/score_calculation/greynet/streams/stream_definition.py new file mode 100644 index 0000000..312e0f4 --- /dev/null +++ b/greyjack/greyjack/score_calculation/greynet/streams/stream_definition.py @@ -0,0 +1,281 @@ +from __future__ import annotations +from abc import ABC, abstractmethod +from typing import Type, Callable, TYPE_CHECKING, Any +from datetime import datetime, timedelta + +from ..common.index_properties import IndexProperties +from ..tuple_tools import get_arity, ARITY_TO_TUPLE, get_facts +from ..nodes.filter_node import FilterNode +from ..nodes.join_node import JoinNode +from ..nodes.group_node import GroupNode +from ..nodes.from_uni_node import FromUniNode +from ..nodes.flatmap_node import FlatMapNode +from ..nodes.conditional_node import ConditionalNode +from ..nodes.sliding_window_node import SlidingWindowNode +from ..nodes.sequence_pattern_node import SequencePatternNode +from .join_adapters import JoinLeftAdapter, JoinRightAdapter +from ..function import Function + +if TYPE_CHECKING: + from .stream import Stream + from ..constraint_factory import ConstraintFactory + from ..core.tuple_pool import TuplePool + from ..core.tuple import AbstractTuple + from ..nodes.abstract_node import AbstractNode + from ..collectors.temporal_collectors import EventSequencePattern + +class StreamDefinition(ABC): + """An abstract base class for defining the behavior of a stream node.""" + def __init__(self, factory: 'ConstraintFactory', source_stream: Stream = None): + self.factory = factory + self.source_stream = source_stream + self.retrieval_id = None # Must be set by subclasses + + @abstractmethod + def build_node(self, node_counter, node_map, scheduler, tuple_pool: TuplePool) -> 'AbstractNode': + """Builds and returns the corresponding Rete node(s) for this definition.""" + pass + + @abstractmethod + def get_target_arity(self) -> int: + """Returns the arity of the tuple this stream will produce.""" + pass + +class FromDefinition(StreamDefinition): + """Definition for a stream originating from facts.""" + def __init__(self, factory: 'ConstraintFactory', fact_class: Type): + super().__init__(factory) + self.fact_class = fact_class + self.retrieval_id = ('from', fact_class) + + def get_target_arity(self) -> int: return 1 + + def build_node(self, node_counter, node_map, scheduler, tuple_pool: 'TuplePool') -> 'AbstractNode': + node = node_map.get(self.retrieval_id) + if node is None: + node_id = node_counter.value; node_counter.value += 1 + node = FromUniNode(node_id, self.fact_class, scheduler, tuple_pool) + node_map[self.retrieval_id] = node + return node + +class FilterDefinition(StreamDefinition): + """Definition for a filter operation.""" + def __init__(self, factory: 'ConstraintFactory', source_stream: 'Stream', predicate: Callable): + super().__init__(factory, source_stream) + self.predicate = predicate + self.retrieval_id = ('filter', source_stream.definition.retrieval_id, predicate) + + def get_target_arity(self) -> int: + return self.source_stream.arity + + def build_node(self, node_counter, node_map, scheduler, tuple_pool: 'TuplePool') -> 'AbstractNode': + node = self.factory.node_sharer.get_or_create_node( + self.retrieval_id, self.factory.node_sharer.alpha_nodes, + lambda: self._create_node(node_counter, scheduler) + ) + if self.retrieval_id not in node_map: + parent_node = self.source_stream.definition.build_node(node_counter, node_map, scheduler, tuple_pool) + parent_node.add_child_node(node) + node_map[self.retrieval_id] = node + return node + + def _create_node(self, node_counter, scheduler) -> 'AbstractNode': + node_id = node_counter.value; node_counter.value += 1 + wrapped_predicate = lambda t: self.predicate(*get_facts(t)) + return FilterNode(node_id, wrapped_predicate, scheduler) + +class JoinDefinition(StreamDefinition): + """Definition for a join operation.""" + def __init__(self, factory: 'ConstraintFactory', left_stream: 'Stream', right_stream: 'Stream', + joiner_type, left_key_func: Callable, right_key_func: Callable): + super().__init__(factory, left_stream) + self.right_stream = right_stream + self.joiner_type = joiner_type + self.left_key_func = left_key_func + self.right_key_func = right_key_func + self.retrieval_id = ( + 'join', left_stream.definition.retrieval_id, right_stream.definition.retrieval_id, + joiner_type, left_key_func, right_key_func + ) + + def get_target_arity(self) -> int: + return self.source_stream.arity + self.right_stream.arity + + def build_node(self, node_counter, node_map, scheduler, tuple_pool: 'TuplePool') -> 'AbstractNode': + node = self.factory.node_sharer.get_or_create_node( + self.retrieval_id, self.factory.node_sharer.beta_nodes, + lambda: self._create_node(node_counter, scheduler, tuple_pool) + ) + if self.retrieval_id not in node_map: + left_node = self.source_stream.definition.build_node(node_counter, node_map, scheduler, tuple_pool) + right_node = self.right_stream.definition.build_node(node_counter, node_map, scheduler, tuple_pool) + left_node.add_child_node(JoinLeftAdapter(node)) + right_node.add_child_node(JoinRightAdapter(node)) + node_map[self.retrieval_id] = node + return node + + def _create_node(self, node_counter, scheduler, tuple_pool) -> 'AbstractNode': + target_arity = self.get_target_arity() + if target_arity > 5: + raise ValueError("Joining would result in an arity greater than 5, which is not supported.") + child_tuple_class = ARITY_TO_TUPLE[target_arity] + + node_id = node_counter.value; node_counter.value += 1 + left_props = IndexProperties(lambda t: self.left_key_func(*get_facts(t))) + right_props = IndexProperties(lambda t: self.right_key_func(*get_facts(t))) + + return JoinNode( + node_id, self.joiner_type, left_props, right_props, + scheduler, tuple_pool, child_tuple_class + ) + +class GroupByDefinition(StreamDefinition): + """Definition for a group_by operation.""" + def __init__(self, factory: 'ConstraintFactory', source_stream: 'Stream', + group_key_function: Callable, collector_supplier: Callable): + super().__init__(factory, source_stream) + self.group_key_function = group_key_function + self.collector_supplier = collector_supplier + self.retrieval_id = ('group_by', source_stream.definition.retrieval_id, group_key_function, collector_supplier) + + def get_target_arity(self) -> int: return 2 + + def build_node(self, node_counter, node_map, scheduler, tuple_pool: 'TuplePool') -> 'AbstractNode': + node = self.factory.node_sharer.get_or_create_node( + self.retrieval_id, self.factory.node_sharer.group_nodes, + lambda: self._create_node(node_counter, scheduler, tuple_pool) + ) + if self.retrieval_id not in node_map: + parent_node = self.source_stream.definition.build_node(node_counter, node_map, scheduler, tuple_pool) + parent_node.add_child_node(node) + node_map[self.retrieval_id] = node + return node + + def _create_node(self, node_counter, scheduler, tuple_pool) -> 'AbstractNode': + node_id = node_counter.value; node_counter.value += 1 + wrapped_fact_group_key = lambda fact: self.group_key_function(fact) + return GroupNode(node_id, wrapped_fact_group_key, self.collector_supplier, scheduler, tuple_pool) + +class ConditionalJoinDefinition(StreamDefinition): + """Definition for an if_exists or if_not_exists operation.""" + def __init__(self, factory: 'ConstraintFactory', source_stream: 'Stream', left_stream: 'Stream', right_stream: 'Stream', + should_exist: bool, left_key_func: Callable, right_key_func: Callable): + super().__init__(factory, source_stream) + self.left_stream = left_stream + self.right_stream = right_stream + self.should_exist = should_exist + self.left_key_func = left_key_func + self.right_key_func = right_key_func + self.retrieval_id = ( + 'cond_join', left_stream.definition.retrieval_id, right_stream.definition.retrieval_id, + should_exist, left_key_func, right_key_func + ) + + def get_target_arity(self) -> int: return self.left_stream.arity + + def build_node(self, node_counter, node_map, scheduler, tuple_pool: 'TuplePool') -> 'AbstractNode': + node = self.factory.node_sharer.get_or_create_node( + self.retrieval_id, self.factory.node_sharer.beta_nodes, + lambda: self._create_node(node_counter, scheduler, tuple_pool) + ) + if self.retrieval_id not in node_map: + left_node = self.left_stream.definition.build_node(node_counter, node_map, scheduler, tuple_pool) + right_node = self.right_stream.definition.build_node(node_counter, node_map, scheduler, tuple_pool) + left_node.add_child_node(JoinLeftAdapter(node)) + right_node.add_child_node(JoinRightAdapter(node)) + node_map[self.retrieval_id] = node + return node + + def _create_node(self, node_counter, scheduler, tuple_pool) -> 'AbstractNode': + node_id = node_counter.value; node_counter.value += 1 + left_props = IndexProperties(lambda t: self.left_key_func(*get_facts(t))) + right_props = IndexProperties(lambda t: self.right_key_func(*get_facts(t))) + return ConditionalNode( + node_id, left_props, right_props, self.should_exist, scheduler + ) + +class FlatMapDefinition(StreamDefinition): + """Definition for a flat_map operation.""" + def __init__(self, factory: 'ConstraintFactory', source_stream: 'Stream', mapper: Callable): + super().__init__(factory, source_stream) + self.mapper = mapper + self.retrieval_id = ('flat_map', source_stream.definition.retrieval_id, mapper) + + def get_target_arity(self) -> int: return 1 + + def build_node(self, node_counter, node_map, scheduler, tuple_pool: 'TuplePool') -> 'AbstractNode': + node = node_map.get(self.retrieval_id) + if node is None: + node = self._create_node(node_counter, scheduler, tuple_pool) + parent_node = self.source_stream.definition.build_node(node_counter, node_map, scheduler, tuple_pool) + parent_node.add_child_node(node) + node_map[self.retrieval_id] = node + return node + + def _create_node(self, node_counter, scheduler, tuple_pool) -> 'AbstractNode': + node_id = node_counter.value; node_counter.value += 1 + + class FlatMapWrapper(Function): + def __init__(self, mapper_callable): + self.mapper = mapper_callable + + def apply(self, parent_tuple: 'AbstractTuple'): + return self.mapper(*get_facts(parent_tuple)) + + final_mapper_obj = FlatMapWrapper(self.mapper) + + return FlatMapNode(node_id, final_mapper_obj, scheduler, tuple_pool) + +class SlidingWindowDefinition(StreamDefinition): + """Definition for a sliding window operation.""" + def __init__(self, factory: 'ConstraintFactory', source_stream: 'Stream', + time_extractor: Callable[[Any], datetime], window_size: timedelta, slide_interval: timedelta): + super().__init__(factory, source_stream) + self.time_extractor = time_extractor + self.window_size = window_size + self.slide_interval = slide_interval + self.retrieval_id = ('sliding_window', source_stream.definition.retrieval_id, + time_extractor, window_size, slide_interval) + + def get_target_arity(self) -> int: return 2 + + def build_node(self, node_counter, node_map, scheduler, tuple_pool: 'TuplePool') -> 'AbstractNode': + node = node_map.get(self.retrieval_id) + if node is None: + node = self._create_node(node_counter, scheduler, tuple_pool) + parent_node = self.source_stream.definition.build_node(node_counter, node_map, scheduler, tuple_pool) + parent_node.add_child_node(node) + node_map[self.retrieval_id] = node + return node + + def _create_node(self, node_counter, scheduler, tuple_pool) -> 'AbstractNode': + node_id = node_counter.value; node_counter.value += 1 + return SlidingWindowNode( + node_id, self.time_extractor, self.window_size, self.slide_interval, scheduler, tuple_pool + ) + +class SequencePatternDefinition(StreamDefinition): + """Definition for a sequence detection operation.""" + def __init__(self, factory: 'ConstraintFactory', source_stream: 'Stream', + pattern: 'EventSequencePattern', time_extractor: Callable[[Any], datetime]): + super().__init__(factory, source_stream) + self.pattern = pattern + self.time_extractor = time_extractor + self.retrieval_id = ('sequence', source_stream.definition.retrieval_id, pattern, time_extractor) + + def get_target_arity(self) -> int: return 1 + + def build_node(self, node_counter, node_map, scheduler, tuple_pool: 'TuplePool') -> 'AbstractNode': + node = node_map.get(self.retrieval_id) + if node is None: + node = self._create_node(node_counter, scheduler, tuple_pool) + parent_node = self.source_stream.definition.build_node(node_counter, node_map, scheduler, tuple_pool) + parent_node.add_child_node(node) + node_map[self.retrieval_id] = node + return node + + def _create_node(self, node_counter, scheduler, tuple_pool) -> 'AbstractNode': + node_id = node_counter.value; node_counter.value += 1 + return SequencePatternNode( + node_id, self.pattern, self.time_extractor, scheduler, tuple_pool + ) diff --git a/greyjack/greyjack/score_calculation/greynet/tuple_tools.py b/greyjack/greyjack/score_calculation/greynet/tuple_tools.py new file mode 100644 index 0000000..a7e28f5 --- /dev/null +++ b/greyjack/greyjack/score_calculation/greynet/tuple_tools.py @@ -0,0 +1,57 @@ +# greynet/tuple_tools.py +from __future__ import annotations +from typing import Type, List, TypeVar +from functools import lru_cache + +from .core.tuple import ( + AbstractTuple, UniTuple, BiTuple, TriTuple, QuadTuple, PentaTuple +) + +# A TypeVar for generic type hints, bound to our base tuple class +T_Tuple = TypeVar('T_Tuple', bound=AbstractTuple) + +# --- Mappings for quick lookups --- +ARITY_TO_TUPLE: dict[int, Type[AbstractTuple]] = { + 1: UniTuple, + 2: BiTuple, + 3: TriTuple, + 4: QuadTuple, + 5: PentaTuple, +} + +TUPLE_TO_ARITY: dict[Type[AbstractTuple], int] = {v: k for k, v in ARITY_TO_TUPLE.items()} + +# --- Core Utility Functions --- + +@lru_cache(maxsize=32) +def get_arity(tuple_class: Type[AbstractTuple]) -> int: + """Gets the arity (number of facts) for a given tuple class.""" + arity = TUPLE_TO_ARITY.get(tuple_class) + if arity is None: + raise TypeError(f"Class {tuple_class.__name__} is not a supported Tuple type.") + return arity + +def get_facts(t: AbstractTuple) -> List: + """Extracts all facts from a tuple instance into a list.""" + # This direct-lookup approach is significantly faster than repeated hasattr checks. + arity = get_arity(type(t)) + if arity == 1: return [t.fact_a] + if arity == 2: return [t.fact_a, t.fact_b] + if arity == 3: return [t.fact_a, t.fact_b, t.fact_c] + if arity == 4: return [t.fact_a, t.fact_b, t.fact_c, t.fact_d] + if arity == 5: return [t.fact_a, t.fact_b, t.fact_c, t.fact_d, t.fact_e] + raise TypeError(f"Cannot get facts for unsupported tuple type: {type(t).__name__}") + +def create_tuple_from_facts(facts: List) -> AbstractTuple: + """Creates a tuple of the correct type based on the number of facts.""" + arity = len(facts) + tuple_class = ARITY_TO_TUPLE.get(arity) + if tuple_class is None: + raise ValueError(f"Cannot create a tuple for arity {arity}. Supported arities are 1-5.") + # Dynamically creates an instance, e.g., BiTuple(facts[0], facts[1]) + return tuple_class(*facts) + +def combine_tuples(left: AbstractTuple, right: AbstractTuple) -> AbstractTuple: + """Combines two tuples into a new, larger tuple.""" + combined_facts = get_facts(left) + get_facts(right) + return create_tuple_from_facts(combined_facts) diff --git a/greyjack/greyjack/score_calculation/score_calculators/GreynetScoreCalculator.py b/greyjack/greyjack/score_calculation/score_calculators/GreynetScoreCalculator.py new file mode 100644 index 0000000..02f5f9a --- /dev/null +++ b/greyjack/greyjack/score_calculation/score_calculators/GreynetScoreCalculator.py @@ -0,0 +1,251 @@ +# greyjack/score_calculation/score_calculators/GreynetScoreCalculator.py + +from __future__ import annotations +from typing import TYPE_CHECKING, Any, Dict, List, Tuple + +from greyjack.score_calculation.scores.ScoreVariants import ScoreVariants +from greyjack.variables.GJFloat import GJFloat +from greyjack.variables.GJInteger import GJInteger +from greyjack.variables.GJBinary import GJBinary +from copy import deepcopy +import numpy as np +from pprint import pprint + +if TYPE_CHECKING: + from greyjack.score_calculation.greynet.builder import ConstraintBuilder + from greyjack.score_calculation.greynet.session import Session + +class GreynetScoreCalculator: + """ + An incremental score calculator that uses the Greynet rule engine. + This calculator holds the Greynet session and provides methods for + the ScoreRequester to interact with it efficiently. + """ + def __init__(self, constraint_builder: 'ConstraintBuilder', score_variant: ScoreVariants): + """ + Initializes the calculator by building the Greynet session from the + provided constraint definitions. + + Args: + constraint_builder (ConstraintBuilder): The Greynet constraint builder + containing all the rules for the problem. + score_variant (ScoreVariants): The score variant enumeration that + corresponds to the score class used in the constraint builder. + """ + from greyjack.score_calculation.greynet.builder import ConstraintBuilder as GreynetConstraintBuilder + if not isinstance(constraint_builder, GreynetConstraintBuilder): + raise TypeError("constraint_builder must be an instance of greynet.ConstraintBuilder") + + self.session: 'Session' = constraint_builder.build() + self.score_variant = score_variant + self.is_incremental = True + self.score_type = self.session.score_class + + # This mapping is populated by the ScoreRequester during initialization. + # It is essential for translating the solver's variable indices to domain objects. + # Key: var_idx (int) -> Value: (fact_object, attribute_name_str) + self.var_idx_to_entity_map: Dict[int, Tuple[Any, str]] = {} + + def initial_load(self, planning_entities: Dict[str, List[Any]], problem_facts: Dict[str, List[Any]]): + """ + Performs the initial population of the Greynet session with all facts + from the problem domain. This should only be called once. + + Args: + planning_entities (dict): A dictionary of planning entity lists. + problem_facts (dict): A dictionary of problem fact lists. + """ + self.session.clear() + + for group_name in problem_facts: + self.session.insert_batch(problem_facts[group_name]) + + for group_name in planning_entities: + self.session.insert_batch(planning_entities[group_name]) + + self.session.flush() + + + def get_score(self) -> Any: + """ + Retrieves the current total score from the Greynet session. + Assumes all pending changes have been flushed. + + Returns: + A score object (e.g., HardSoftScore) representing the current state. + """ + #self.session.recalculate_all_scores() + score = self.session.get_score() + return score + + def _full_sync_and_get_score(self, sample: List[float]) -> Any: + """ + A non-incremental way to get a score for a full solution vector. + This modifies the session state and is primarily for debugging or fallback. + """ + changed_facts, original_vals = self._apply_deltas_internal(list(enumerate(sample))) + score = self.get_score() + self._revert_deltas_internal(changed_facts, original_vals) + return score + + def _apply_and_get_score_for_batch(self, deltas: List[List[Tuple[int, float]]]) -> List[Any]: + """ + Applies a batch of deltas, gets the score for each, and reverts the state + between each delta application. This is the primary method for incremental scoring. + """ + scores = [] + for delta_set in deltas: + if not delta_set: + scores.append(self.get_score()) + continue + + changed_facts, original_values = self._apply_deltas_internal(delta_set) + scores.append(self.get_score()) + self._revert_deltas_internal(changed_facts, original_values) + + + return scores + + def map_deltas_to_entities(self, deltas: List[Tuple[int, float]]) -> List[Any]: + + entity_objects: List[Any] = [] + + for var_idx, new_value in deltas: + entity, attr_name = self.var_idx_to_entity_map[var_idx] + mapped_entity = deepcopy(entity) + setattr(mapped_entity, attr_name, new_value) + entity_objects.append(mapped_entity) + + return entity_objects + + def update_entity_mapping_plain(self, sample): + delta_updates = list(enumerate(sample)) + + for var_idx, new_value in delta_updates: + entity, attr_name = self.var_idx_to_entity_map[var_idx] + setattr(entity, attr_name, new_value) + + self._apply_deltas_internal(delta_updates) + + def update_entity_mapping_incremental(self, deltas): + + for var_idx, new_value in deltas: + entity, attr_name = self.var_idx_to_entity_map[var_idx] + setattr(entity, attr_name, new_value) + + + def _apply_deltas_internal(self, deltas: List[Tuple[int, float]]) -> Tuple[List[Any], List[Any]]: + """ + Internal helper to apply changes to the session state by creating modified + copies of facts, retracting the originals, and inserting the copies. + + Returns: + A tuple containing (list of original facts, list of changed facts) for reverting. + """ + original_to_changed_map: Dict[Any, Any] = {} + for var_idx, new_value in deltas: + entity, attr_name = self.var_idx_to_entity_map[var_idx] + if entity not in original_to_changed_map: + original_to_changed_map[entity] = deepcopy(entity) + + setattr(original_to_changed_map[entity], attr_name, new_value) + + originals = list(original_to_changed_map.keys()) + changed = list(original_to_changed_map.values()) + + #pprint(self.session.get_constraint_matches()) + + if originals: + #print_internals_from_entity_mapping(self.var_idx_to_entity_map) + #print_internals(originals, "row_id") + #score_1 = self.get_score() + self.session.retract_batch(originals) + #self.session.flush() + #score_2 = self.get_score() + + #print_internals(changed, "row_id") + self.session.insert_batch(changed) + self.session.flush() + #score_3 = self.get_score() + + #print() + #self.session.recalculate_all_scores() + + return originals, changed + + def _revert_deltas_internal(self, originals: List[Any], changed: List[Any]): + """ + Internal helper to revert the changes made by _apply_deltas_internal. + It retracts the modified copies and re-inserts the original facts. + """ + if changed: + + #score_1 = self.get_score() + self.session.retract_batch(changed) + #self.session.flush() + #score_2 = self.get_score() + + self.session.insert_batch(originals) + self.session.flush() + #score_3 = self.get_score() + + #print() + pass + #self.session.recalculate_all_scores() + + def commit_deltas(self, deltas): + + if not deltas: + return + + original_to_changed_map = {} + for var_idx, new_value in deltas: + entity, attr_name = self.var_idx_to_entity_map[var_idx] + if entity not in original_to_changed_map: + original_to_changed_map[entity] = deepcopy(entity) + + setattr(original_to_changed_map[entity], attr_name, new_value) + + originals = list(original_to_changed_map.keys()) + changed = list(original_to_changed_map.values()) + + #for i in range(len(originals)): + # tmp = changed[i].greynet_fact_id + # changed[i].greynet_fact_id = originals[i].greynet_fact_id + # originals[i].greynet_fact_id = tmp + + if originals: + self.session.retract_batch(originals) + #self.session.flush() + self.session.insert_batch(changed) + self.session.flush() + + original_id_to_new_entity_map = {id(orig): new for orig, new in zip(originals, changed)} + for var_idx, (entity, attr_name) in self.var_idx_to_entity_map.items(): + original_id = id(entity) + if original_id in original_id_to_new_entity_map: + self.var_idx_to_entity_map[var_idx] = (original_id_to_new_entity_map[original_id], attr_name) + + #self.session.recalculate_all_scores() + +# for debug +@staticmethod +def print_internals(entities, attr_name): + + values = [] + for entity in entities: + value = getattr(entity, attr_name) + values.append(value) + + print(values) + +@staticmethod +def print_internals_from_entity_mapping(var_idx_to_entity_map): + + values = [] + for var_idx in var_idx_to_entity_map.keys(): + entity, attr_name = var_idx_to_entity_map[var_idx] + value = getattr(entity, attr_name) + values.append(value) + + print(values) diff --git a/greyjack/greyjack/score_calculation/score_requesters/OOPScoreRequester.py b/greyjack/greyjack/score_calculation/score_requesters/OOPScoreRequester.py index 1701bc0..8a91889 100644 --- a/greyjack/greyjack/score_calculation/score_requesters/OOPScoreRequester.py +++ b/greyjack/greyjack/score_calculation/score_requesters/OOPScoreRequester.py @@ -1,17 +1,98 @@ +# greyjack/score_calculation/score_requesters/OOPScoreRequester.py import polars as pl from greyjack.greyjack import VariablesManagerPy, CandidateDfsBuilderPy from greyjack.variables.GJFloat import GJFloat from greyjack.variables.GJInteger import GJInteger from greyjack.variables.GJBinary import GJBinary +from greyjack.score_calculation.score_calculators.GreynetScoreCalculator import GreynetScoreCalculator +import traceback class OOPScoreRequester: def __init__(self, cotwin): self.cotwin = cotwin - - self.available_planning_variable_types = {GJFloat, GJInteger, GJBinary} + self.is_greynet = isinstance(self.cotwin.score_calculator, GreynetScoreCalculator) + + # This initialization logic is common to both modes variables_vec, var_name_to_vec_id_map, vec_id_to_var_name_map = self.build_variables_info(self.cotwin) self.variables_manager = VariablesManagerPy(variables_vec) + self.vec_id_to_var_name_map = vec_id_to_var_name_map + + if self.is_greynet: + self._init_greynet() + else: + self._init_plain(variables_vec, var_name_to_vec_id_map, vec_id_to_var_name_map) + + def _init_greynet(self): + """Initializes the requester and calculator for Greynet mode.""" + calculator = self.cotwin.score_calculator + + try: + initialized_planning_entities = {} + for group_name in self.cotwin.planning_entities: + current_initialized_entities = self.build_initialized_entities(self.cotwin.planning_entities, group_name) + initialized_planning_entities[group_name] = current_initialized_entities + + # Build the crucial mapping from solver's variable index to the domain object + var_idx_to_entity_map = {} + i = 0 + greynet_fact_id = 0 + for group_name in self.cotwin.planning_entities: + for native_entity, initialized_entity in zip(self.cotwin.planning_entities[group_name], initialized_planning_entities[group_name]): + for attr_name, attr_value in native_entity.__dict__.items(): + if type(attr_value) in {GJFloat, GJInteger, GJBinary}: + var_idx_to_entity_map[i] = (initialized_entity, attr_name) + i += 1 + initialized_entity.greynet_fact_id = greynet_fact_id + greynet_fact_id += 1 + calculator.var_idx_to_entity_map = var_idx_to_entity_map + except: + print(traceback.format_exc()) + + # Perform the initial full load of the Greynet session + calculator.initial_load(initialized_planning_entities, self.cotwin.problem_facts) + + def build_initialized_entities(self, planning_entities, group_name): + + current_planning_entities_group = planning_entities[group_name] + initialized_entities = [] + + for entity in current_planning_entities_group: + new_entity = self.build_initialized_entity(entity) + initialized_entities.append(new_entity) + + return initialized_entities + + def build_initialized_entity(self, entity): + + entity_attributes_dict = entity.__dict__ + + new_entity_kwargs = {} + for attribute_name in entity_attributes_dict: + attribute_value = entity_attributes_dict[attribute_name] + + if type(attribute_value) in {GJFloat, GJInteger, GJBinary}: + value = attribute_value.planning_variable.initial_value + + if value is None: + raise ValueError("All planning variables must have initial value for scoring by greynet") + else: + value = attribute_value + + new_entity_kwargs[attribute_name] = value + del new_entity_kwargs["greynet_fact_id"] + + new_entity = type(entity)(**new_entity_kwargs) + new_entity.greynet_fact_id = entity.greynet_fact_id + + return new_entity + + def set_correct_greynet_state(self, chosen_deltas): + calculator = self.cotwin.score_calculator + calculator._apply_deltas_internal(chosen_deltas) + + def _init_plain(self, variables_vec, var_name_to_vec_id_map, vec_id_to_var_name_map): + """Initializes the requester for the standard DataFrame-based calculation.""" planning_entities_column_map, entity_is_int_map = self.build_column_map(self.cotwin.planning_entities) problem_facts_column_map, _ = self.build_column_map(self.cotwin.problem_facts) planning_entity_dfs = self.build_group_dfs(self.cotwin.planning_entities, planning_entities_column_map, True) @@ -27,12 +108,31 @@ def __init__(self, cotwin): problem_fact_dfs, entity_is_int_map ) + + def request_score_plain(self, samples): + if self.is_greynet: + # Delegate to the calculator's full sync method for each sample + return [self.cotwin.score_calculator._full_sync_and_get_score(s) for s in samples] + else: + # Use the existing Rust-based DataFrame builder + planning_entity_dfs, problem_fact_dfs = self.candidate_dfs_builder.get_plain_candidate_dfs(samples) + return self.cotwin.get_score_plain(planning_entity_dfs, problem_fact_dfs) + def request_score_incremental(self, sample, deltas): + if self.is_greynet: + # Delegate the entire batch of deltas to the calculator + return self.cotwin.score_calculator._apply_and_get_score_for_batch(deltas) + else: + # Use the existing Rust-based incremental logic + planning_entity_dfs, problem_fact_dfs, delta_dfs_for_rust = self.candidate_dfs_builder.get_incremental_candidate_dfs(sample, deltas) + return self.cotwin.get_score_incremental(planning_entity_dfs, problem_fact_dfs, delta_dfs_for_rust) + + # The following methods are shared helpers and remain unchanged. def build_variables_info(self, cotwin): + # ... (implementation is identical to the one provided) variables_vec = [] var_name_to_vec_id_map = {} vec_id_to_var_name_map = {} - i = 0 for planning_entities_group_name in cotwin.planning_entities: current_planning_entities_group = cotwin.planning_entities[planning_entities_group_name] @@ -40,75 +140,54 @@ def build_variables_info(self, cotwin): entity_attributes_dict = entity.__dict__ for attribute_name in entity_attributes_dict: attribute_value = entity_attributes_dict[attribute_name] - if type(attribute_value) not in self.available_planning_variable_types: - continue - variable = attribute_value - full_variable_name = planning_entities_group_name + ": " + str(i) + "-->" + attribute_name - variable.planning_variable.name = full_variable_name - var_name_to_vec_id_map[full_variable_name] = i - vec_id_to_var_name_map[i] = full_variable_name - variables_vec.append(variable.planning_variable) - i += 1 - + if type(attribute_value) in {GJFloat, GJInteger, GJBinary}: + variable = attribute_value + full_variable_name = f"{planning_entities_group_name}: {i}-->{attribute_name}" + variable.planning_variable.name = full_variable_name + var_name_to_vec_id_map[full_variable_name] = i + vec_id_to_var_name_map[i] = full_variable_name + variables_vec.append(variable.planning_variable) + i += 1 return variables_vec, var_name_to_vec_id_map, vec_id_to_var_name_map def build_column_map(self, entity_groups): - + # ... (implementation is identical to the one provided) column_dict = {} entity_is_int_map = {} for group_name in entity_groups: column_dict[group_name] = [] entity_objects = entity_groups[group_name] + if not entity_objects: continue sample_object = entity_objects[0] object_attributes = sample_object.__dict__ - for attribute_name in object_attributes: - column_dict[group_name].append( attribute_name ) - attribute_value = object_attributes[attribute_name] + for attribute_name, attribute_value in object_attributes.items(): + column_dict[group_name].append(attribute_name) if isinstance(attribute_value, GJFloat): entity_is_int_map[attribute_name] = False else: entity_is_int_map[attribute_name] = True - return column_dict, entity_is_int_map def build_group_dfs(self, entity_groups, column_dict, is_planning): - + # ... (implementation is identical to the one provided) df_dict = {} - for df_name in column_dict: column_names = column_dict[df_name] df_data = [] - entity_group = entity_groups[df_name] for entity_object in entity_group: row_data = [] object_attributes = entity_object.__dict__ for column_name in column_names: attribute_value = object_attributes[column_name] - if type(attribute_value) in self.available_planning_variable_types: + if type(attribute_value) in {GJFloat, GJInteger, GJBinary}: attribute_value = None - row_data.append( attribute_value ) + row_data.append(attribute_value) if is_planning: row_data = [0] + row_data - df_data.append( row_data ) - if is_planning: - column_names = ["sample_id"] + column_names - df = pl.DataFrame( data=df_data, schema=column_names, orient="row" ) - + df_data.append(row_data) + schema = ["sample_id"] + column_names if is_planning else column_names + df = pl.DataFrame(data=df_data, schema=schema, orient="row") df_dict[df_name] = df - - return df_dict - def request_score_plain(self, samples): - - planning_entity_dfs, problem_fact_dfs = self.candidate_dfs_builder.get_plain_candidate_dfs(samples) - score_batch = self.cotwin.get_score_plain(planning_entity_dfs, problem_fact_dfs) - return score_batch - - def request_score_incremental(self, sample, deltas): - - planning_entity_dfs, problem_fact_dfs, delta_dfs = self.candidate_dfs_builder.get_incremental_candidate_dfs(sample, deltas) - score_batch = self.cotwin.get_score_incremental(planning_entity_dfs, problem_fact_dfs, delta_dfs) - return score_batch - diff --git a/greyjack/pyproject.toml b/greyjack/pyproject.toml index c959394..1b3bab5 100644 --- a/greyjack/pyproject.toml +++ b/greyjack/pyproject.toml @@ -7,14 +7,19 @@ features = ["pyo3/extension-module"] [project] name = "greyjack" -version = "0.2.6" +version = "0.3.3" requires-python = ">=3.9" dependencies = [ + "bitarray>=3.5.0", "dill", + "matplotlib>=3.9.4", + "maturin>=1.9.1", + "mmh3>=5.1.0", "multiprocess", "mypy", "mypy-extensions", "mypy-protobuf", + "numba>=0.60.0", "numpy", "pathos", "polars", @@ -58,4 +63,4 @@ classifiers = [ Homepage = "https://github.com/CameleoGrey/greyjack-solver-python" Documentation = "https://github.com/CameleoGrey/greyjack-solver-python" Repository = "https://github.com/CameleoGrey/greyjack-solver-python.git" -Issues = "https://github.com/CameleoGrey/greyjack-solver-python/issues" \ No newline at end of file +Issues = "https://github.com/CameleoGrey/greyjack-solver-python/issues" diff --git a/greyjack/src/agents/base/metaheuristic_bases/concrete_tabu_search_base_macros.rs b/greyjack/src/agents/base/metaheuristic_bases/concrete_tabu_search_base_macros.rs index 26bcfbd..b4ece4b 100644 --- a/greyjack/src/agents/base/metaheuristic_bases/concrete_tabu_search_base_macros.rs +++ b/greyjack/src/agents/base/metaheuristic_bases/concrete_tabu_search_base_macros.rs @@ -138,30 +138,30 @@ macro_rules! build_concrete_tabu_search_base { sample: Vec, deltas: Vec>, scores: Vec<$score_type>, - ) -> Vec<$individual_variant> { + ) -> (Vec<$individual_variant>, Option>) { - let best_score_id: usize = scores .iter() .enumerate() .min_by(|(_, a), (_, b)| a.cmp(b)) .map(|(index, _)| index) .unwrap(); + let mut sample = sample; let best_score = scores[best_score_id].clone(); - let new_population:Vec<$individual_variant>; + if best_score <= current_population[0].score { - let best_deltas = &deltas[best_score_id]; - for (var_id, new_value) in best_deltas { + let new_values = deltas[best_score_id].clone(); + for (var_id, new_value) in &new_values { sample[*var_id] = *new_value; } let best_candidate = $individual_variant::new(sample.clone(), best_score); - new_population = vec![best_candidate; 1]; + let new_population = vec![best_candidate; 1]; + + (new_population, Some(new_values)) } else { - new_population = current_population.clone(); + (current_population.clone(), None) } - - return new_population; } #[getter] diff --git a/greyjack/src/score_calculation/scores/hard_medium_soft_score.rs b/greyjack/src/score_calculation/scores/hard_medium_soft_score.rs index 4ddf08e..75ff02f 100644 --- a/greyjack/src/score_calculation/scores/hard_medium_soft_score.rs +++ b/greyjack/src/score_calculation/scores/hard_medium_soft_score.rs @@ -28,6 +28,15 @@ impl HardMediumSoftScore { } } + #[staticmethod] + pub fn get_score_fields() -> Vec { + vec![ + "hard_score".to_string(), + "medium_score".to_string(), + "soft_score".to_string(), + ] + } + #[getter] pub fn get_hard_score(&self) -> f64 { self.hard_score diff --git a/greyjack/src/score_calculation/scores/hard_soft_score.rs b/greyjack/src/score_calculation/scores/hard_soft_score.rs index 2d154cf..f9b4997 100644 --- a/greyjack/src/score_calculation/scores/hard_soft_score.rs +++ b/greyjack/src/score_calculation/scores/hard_soft_score.rs @@ -25,6 +25,11 @@ impl HardSoftScore { } } + #[staticmethod] + pub fn get_score_fields() -> Vec { + vec!["hard_score".to_string(), "soft_score".to_string()] + } + #[getter] pub fn get_hard_score(&self) -> f64 { self.hard_score diff --git a/greyjack/src/score_calculation/scores/simple_score.rs b/greyjack/src/score_calculation/scores/simple_score.rs index db71f1a..dedb7e3 100644 --- a/greyjack/src/score_calculation/scores/simple_score.rs +++ b/greyjack/src/score_calculation/scores/simple_score.rs @@ -24,6 +24,11 @@ impl SimpleScore { } } + #[staticmethod] + pub fn get_score_fields() -> Vec { + vec!["simple_value".to_string()] + } + #[getter] pub fn get_simple_value(&self) -> f64 { self.simple_value diff --git a/greyjack/uv.lock b/greyjack/uv.lock new file mode 100644 index 0000000..f50515b --- /dev/null +++ b/greyjack/uv.lock @@ -0,0 +1,1647 @@ +version = 1 +revision = 1 +requires-python = ">=3.9" +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", + "python_full_version < '3.10'", +] + +[[package]] +name = "bitarray" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0d/0f/6ecf00ec04622b8309aca3cfbdba20d5399d9e4e5a4b156d9ffd2e5610d3/bitarray-3.5.0.tar.gz", hash = "sha256:e10ae216416c36500c86c08ffceaf7589f6ad54056a7007845cdd907813e7d25", size = 148024 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/07/9feead3924e739c60691130bff2d5f704a810282275c31aa8d590a05c22c/bitarray-3.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9bb632a55ed7250d43acdfef3e566d546e5f89275ef49e903f7aa19a8bba48f6", size = 144699 }, + { url = "https://files.pythonhosted.org/packages/5b/86/c4c5e199ed1d35073f17060cd07cf531bf5b6d8ef2160e33f435b32deb66/bitarray-3.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:550d41ed332d7065f1b7fc0ff806894df09282981bb301915bd9a4978a5338c1", size = 141320 }, + { url = "https://files.pythonhosted.org/packages/10/a1/0caf48e7d4c033a92b85f8480f44b42edbed1c67edcf7a01382cc3ced2ba/bitarray-3.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b1f016c17c22c018cc6cd555d14cf5281ccbe02e7f15c5ae0f8bbb2b84eca4e", size = 310634 }, + { url = "https://files.pythonhosted.org/packages/f5/e0/4577719a620dc0499c33d0b2803e5912c0cae0fa4d2f8d8716cf1f270b4a/bitarray-3.5.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2a8150368e30b7503159bb6f54f824666d043b6275bbcc50582bbfa36739ca87", size = 326566 }, + { url = "https://files.pythonhosted.org/packages/cb/04/208a6f10acaac1f6ea1cb1def0e4c569c00f0986ceacd7ff9c531f298109/bitarray-3.5.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d4d9a3b0ca1ed1d0f9d80265b1cc40b1483cce2e8dc5d0dac99ae6f0dd0ffeca", size = 318847 }, + { url = "https://files.pythonhosted.org/packages/c7/b6/c97ac45534569e0104ddaffe3ec507170d875c395c3fadd9220dfe288820/bitarray-3.5.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f181b99cfd35a9f824faa598954364eeb67f3d92f5256c19b949f719e1031e6", size = 312158 }, + { url = "https://files.pythonhosted.org/packages/52/85/095d4d25c2dd01aa7e29d55c29650eec908eceaf902dc0d94a8798c2589b/bitarray-3.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a11d4800be04a1e216f90266eef27bc98c8502455f0ff9d1dbc72ae566efeb9e", size = 299708 }, + { url = "https://files.pythonhosted.org/packages/4f/65/6df92197a262daf2b47fe7ba350cfe0d172e5d5c6f60053c6b5814792dcb/bitarray-3.5.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a44c4bc66150fbc10221214ae763c1a4cb9f52a1e79da5814974d277c30169dd", size = 304786 }, + { url = "https://files.pythonhosted.org/packages/4b/c2/c25ff869118a1c76461f2e769ca73b3c51bfd45d3c4fb14b2ab52db302b4/bitarray-3.5.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:ad02bcac204496986a60415e4c9884b7366e4bc9a2e5de75eaa5dc46555dcef5", size = 296310 }, + { url = "https://files.pythonhosted.org/packages/44/2d/a828ce97df6fda6713e78e4fddf49248b2c01f9617d6a09897262a1b8eae/bitarray-3.5.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:b8e1a8e74d9a56c4b524ad43b35cd095022102fe2dccf7737fc8da32e80edb2e", size = 321572 }, + { url = "https://files.pythonhosted.org/packages/42/69/f7ab446b7592c3410ca18d8dd7c85672854be79bda2eb367e915586b0bbb/bitarray-3.5.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:825b2e17cf7f71e5a1e431b3c874c3bef93cd5d63615f08eab884b85ba1b9b7d", size = 324081 }, + { url = "https://files.pythonhosted.org/packages/38/e7/29d68097bf1997014ba92ade78abb5f553038c68349cc99455bbd457d83c/bitarray-3.5.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0f6ae329ec0f2ed6f8b77ef44fc49e072fa16ded0ed8dceb6ade7c994aa506c2", size = 303688 }, + { url = "https://files.pythonhosted.org/packages/b2/b8/bfaf67ba500770f37d07e52bfd92e1e13542a37edcf38f83a39c32942eb6/bitarray-3.5.0-cp310-cp310-win32.whl", hash = "sha256:46de32d5065528fb6b45318fc603bf9059893bf8dfd0825ab8ffabda50d194f5", size = 138167 }, + { url = "https://files.pythonhosted.org/packages/bc/f5/84226380489d5c9aca51905f4721920f1c1b6154c779f854093b9e59bf6c/bitarray-3.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:59841744a6b240bb99e54a716cad1ffa11f01bb64df647f74539948c5a0caa8f", size = 144744 }, + { url = "https://files.pythonhosted.org/packages/cc/49/ec0652fff05d64525779ac336818ce0de88c6951f3ca8db648b7fd432be6/bitarray-3.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:24a3dbada4b8a65901e7c39eb6a0efb8a151bbec93b86a31a5790eaa41616447", size = 144696 }, + { url = "https://files.pythonhosted.org/packages/21/39/e6859818865e59a70b86953bd37400efef3eaa391add4955027233ff46c1/bitarray-3.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3fafe54f5a4af6aee0d162f6962d0e919a90c3f42e1b2a1064f1b2b47914f7d9", size = 141322 }, + { url = "https://files.pythonhosted.org/packages/ac/49/061ca66c809baa76af69bc736eda25307e848ab0c7baedacdc052c7e3e49/bitarray-3.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d27236276cdaf343b75f747f0359dbe717dd9adeda87dafa29971c90eb4da05", size = 318276 }, + { url = "https://files.pythonhosted.org/packages/38/e7/8ab419b8607e7215e5b1651b22d551916eda7f43ad87b6f2f5b712f2ebed/bitarray-3.5.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:044dc0fd286df53973f3935dc71c41f0aa49f5480f465f9f9dd7b66d8154a597", size = 334608 }, + { url = "https://files.pythonhosted.org/packages/44/85/01a7a208c890c66bc9bfb22c2387c22d18e10bd78122bdc980a2275b0bc4/bitarray-3.5.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8bd508fdc7fb6e49e0a1121a0fdb0106929f05e1885e401b5cb097d6cf49a201", size = 327586 }, + { url = "https://files.pythonhosted.org/packages/b8/e8/7dc417bd3dd1833f2dd1aec65c313eec0b7c15b5c214b1370bd9187de294/bitarray-3.5.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8b124f6296961859f34195df6a5c72ca671fe084c6242a15028ed7aed257828", size = 319977 }, + { url = "https://files.pythonhosted.org/packages/88/a0/048353fcf3294cc3bcdfa7e25eb5f0e894e8e9fa49e51b00f7802445c1a1/bitarray-3.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bdeaf551344860a2da79d9306cafb3673c5719de2d07f16ad544fdbd3d10ac4e", size = 307848 }, + { url = "https://files.pythonhosted.org/packages/ce/88/2f9aed7798fdf4def87b6046c41b6dcba7e379a42ef4126611768d74cda3/bitarray-3.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0570482325465361979a1a92b97b4a23ed8cba1965e506ae7689029fdf1369e7", size = 312889 }, + { url = "https://files.pythonhosted.org/packages/c5/75/8b1af14be525b9ab30c2c631c4ff521ef07af67ef13509e3472acb02821f/bitarray-3.5.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f3c0561ac6c22e55353789ef8a85cec4dcbadf20f3b7913fcfb0871d71dfd6ae", size = 303998 }, + { url = "https://files.pythonhosted.org/packages/86/76/e20d5a3fbb9653bd7dd0b99fb2bd6b4c281bbfdcb60c7c8b3692ea52d4b4/bitarray-3.5.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:eae607b47620907a90d03ff9b097a742506ae45cb18b049b18d5668802e418c0", size = 329405 }, + { url = "https://files.pythonhosted.org/packages/bb/1f/edd9b472f23b80d5197c7f46a7c5544e46a8c051296784621a7c4904c70f/bitarray-3.5.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:4f45e1010543f0c742425e1bc85796bb3b9f0acfefa6f518d3da9b317528a4ed", size = 332278 }, + { url = "https://files.pythonhosted.org/packages/7b/91/eb367694f0253e5e0c2923aada02182703e70c66da3b6f6eb3ea6f7bc58b/bitarray-3.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:609ab7754decc5adb66d440853f303ea249e0125cb6cf17da28530964b2c4652", size = 311828 }, + { url = "https://files.pythonhosted.org/packages/57/d3/57dc1d0585298e6ee784abadaa87c6e5d4564c17d50f0dfd453787c4510f/bitarray-3.5.0-cp311-cp311-win32.whl", hash = "sha256:70a283cd08d88bb197dd68a21382ca5ab74aaf641f69229e8675c685891ed8f5", size = 138332 }, + { url = "https://files.pythonhosted.org/packages/91/35/7960be0db242881ac1406aa33ba35bbb22725a673ab870f47efa8b6e6a54/bitarray-3.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:b4d245dfcd8203ea005da3b34bf76364c6765305a96cabd539e40fd4178a9ff0", size = 144967 }, + { url = "https://files.pythonhosted.org/packages/4d/47/3b2fd638a96da2e42a53647d694b3ec6faced228cae608889e685f03351b/bitarray-3.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cc918ff7beaa389055f38d7bb096b00b95434d80a6f4784624dbca4fb919464c", size = 144424 }, + { url = "https://files.pythonhosted.org/packages/98/13/e4f2211629cad18655a677c1b0135d8e82c0ab6c851d496227f6ea8cd35d/bitarray-3.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5964f319ef78cf69f24eebb9061e06462eed1a9ca62bdbbb6dcad611691f271d", size = 141304 }, + { url = "https://files.pythonhosted.org/packages/d4/79/e5625aabf5ca1f79b44d6b5c5f4c7fd611f3d574afe241edacd2b31a3531/bitarray-3.5.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e0099251d50ddcab672d6ddf415eccf1b1117f0ab54e9321f905685d9ff910a6", size = 321007 }, + { url = "https://files.pythonhosted.org/packages/5c/63/dde5698a9f3a3bb794f48f16f8d23ff42f26ea99a2fa7a7632dc3ea275e7/bitarray-3.5.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:923fa335dcc3ecbb516f12b2b70f5991773ffe05484f8c8d9d31bd419c311e90", size = 336578 }, + { url = "https://files.pythonhosted.org/packages/ca/8c/7e0dd6dee3e4311224ddf95601cd60c0378db3d591cdc135b70abc7f3999/bitarray-3.5.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:352a6f7a5501ab4f74fda2275258fb7607b740ca74f5768d10774efca4215d4e", size = 330179 }, + { url = "https://files.pythonhosted.org/packages/bb/da/8b43649052792e9786b4cbf895191d7ee9d907a5bc802059ff3eeb57e2af/bitarray-3.5.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:403b2045f43d70b184f559bfc39cb98fef02ee03aafd1cbe6d77db345a8c8275", size = 323080 }, + { url = "https://files.pythonhosted.org/packages/fa/a9/c647eb2dd958550173c01f4859a8406b41ac8caa37a00d2be48711a31828/bitarray-3.5.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:25946a0faf22f16619a1eae82ae28c0e5e09815e7d5629c507df7b4e3bc7807a", size = 310444 }, + { url = "https://files.pythonhosted.org/packages/c0/46/115dff1e6c21461a0b1681fb4ec666d50bdda5dce8c6c0e1b384e753b145/bitarray-3.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:18573b66d9c08b692e21abff8b77f32716ec77ab8aff60916590107d725bf648", size = 315128 }, + { url = "https://files.pythonhosted.org/packages/70/78/33e485dbe8e499677271b9b7d244b3cb57d57b0396672682a071a2b22cde/bitarray-3.5.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:39d8b00e5031d0ab8101c2e6349b04cd753d2426b963279858ed1d5b0d454cd8", size = 306860 }, + { url = "https://files.pythonhosted.org/packages/ce/11/50234d6d1c057aee92a0aa7849a7276352db3772267818ac066aee24fefa/bitarray-3.5.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e75f9150b59a4f5d205bd55e5ddb741e295045f17ebb1d4599e7e68563e98955", size = 331374 }, + { url = "https://files.pythonhosted.org/packages/12/38/2bc7a39182fbfd7fbe16b7f400034984af29e9dc640f56a349529fe13d27/bitarray-3.5.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:67343d181a4181ee608afe3a284b21d8be0ed11bdfbf9dd0779d57403556fedf", size = 335026 }, + { url = "https://files.pythonhosted.org/packages/f5/d3/b4c16d161d7b8bbf019532faf933ad7306ecbf1ae24c10d1a0ec75bb0906/bitarray-3.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:333c707bbc58028750e56f2da253838a71ec47c6dd6f8302c461381258e266f7", size = 314989 }, + { url = "https://files.pythonhosted.org/packages/5e/01/94be9b1fbe81143a8d842cbfa8b14fdca12b96c848239b71b944059aa310/bitarray-3.5.0-cp312-cp312-win32.whl", hash = "sha256:891817b3006f56e542f889d844c4c5c4715ea7b6e0e5825fc51dd1f7de76c14c", size = 138373 }, + { url = "https://files.pythonhosted.org/packages/9c/20/58a93d971e9d89a94c65d83a740a161370492ed51139ab42260d93de85ab/bitarray-3.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:86f043ed3de669eaa1ace14bc0c3b2c1b21f53728e48f00699117f94c8c0e02b", size = 145176 }, + { url = "https://files.pythonhosted.org/packages/26/02/a3946dcf6176363df87f2e0a7e4f001ac25e54fef0218ae5660d763e1ded/bitarray-3.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9bf8439bc80fd7a075c180a27d7b951e2ee133be6c1c102443de6e0237390284", size = 144406 }, + { url = "https://files.pythonhosted.org/packages/ec/21/b14f7ca5726ab569c3c61f3661de7bea3719d12a2988c2c653ca0fa19f81/bitarray-3.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3ef64b8a8715b8d2393ab94b5e9c0463e43eba0dbe1e01dde93bbadb73c39cc3", size = 141298 }, + { url = "https://files.pythonhosted.org/packages/58/98/79eced6eddfc71508b021d20d385413938347d5ff805eddb15ae4c4f47e7/bitarray-3.5.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eca3558001e7228ffd917a4db91992e0955d02c6ae878e9788460ee7499f1665", size = 320951 }, + { url = "https://files.pythonhosted.org/packages/a2/07/4b696f7c2b01706ced6d1f06f8c68b12cb1822b78e8dc85909397b3d8a2e/bitarray-3.5.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8a1cd11d51721bc2402d8bd5b0d17b855cecb63366f711cf016546da84bb4548", size = 336462 }, + { url = "https://files.pythonhosted.org/packages/79/7d/7898927a7f4eaa9408c65dbecd4dab6e5d9273e86b02cf74f86113f97363/bitarray-3.5.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3a23cc993ff727e7e29058ca8c663613ffc3725e02e0eba6c0650bb538a1b2e7", size = 330024 }, + { url = "https://files.pythonhosted.org/packages/f0/db/b49e7aa669bfcb176099b296b4dafdc7783633e482fed4b6dc0f4f19a1aa/bitarray-3.5.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f47e2f676a3cdc8c21771d3a7e736af5c8385122a8de4f0c11a965ac0905888", size = 322969 }, + { url = "https://files.pythonhosted.org/packages/25/b8/5c1110467b142809c80db5eb9234c1592ae24a0feb4ed47e8590da841866/bitarray-3.5.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:287ee0717df4d70a7260495006c7dfbbeb42ba9e067224359f923e2ce76f1f0e", size = 310293 }, + { url = "https://files.pythonhosted.org/packages/8c/74/9750132b2ca7d6d3a213ce144281f1bee7b1bfba2e8693f1b219d6c0ca16/bitarray-3.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7f45aee9532f0f4f3b0dc7058eb1730661c0a83d29a2cc00c6fbb80c3080e85b", size = 315140 }, + { url = "https://files.pythonhosted.org/packages/84/05/69616222f43b11f4ae4d88408f30b551de96c6d4d4fe98b70774004aee2c/bitarray-3.5.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a4a8dc574ad6928dc327bffbd32d3bab8cdc21e90b9c773a7e40f8c2627f572e", size = 306878 }, + { url = "https://files.pythonhosted.org/packages/b2/6e/39bf609510ae3b30ba42f3f26b906da155ed4abda4d48bf9418e0791df8b/bitarray-3.5.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:02243c8db4a43c2f1ac66396073ac68d1e41863eef248a1f5fdf859c0b3edc33", size = 331380 }, + { url = "https://files.pythonhosted.org/packages/5f/b6/f636072cc9d63598088a2869dd2011078b89b4c4293865c4482cb075b93d/bitarray-3.5.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:8fdd640b86637d9f20e620cce4efad36e1b147feb978153222195a0703a5c013", size = 334983 }, + { url = "https://files.pythonhosted.org/packages/6a/c1/a800f2d0149ca944fb18827c328803bbc9ed8bb84a80489d2ca7dbe0062a/bitarray-3.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:57312af2775edf4842f2561e996341394bc9650330bd352f38b6144238f95ec3", size = 315012 }, + { url = "https://files.pythonhosted.org/packages/38/9a/814c9ea5b3c9ab300a06f19a7e3f1a74424f1d46659d56b7409e989adb1a/bitarray-3.5.0-cp313-cp313-win32.whl", hash = "sha256:190eb9ce58f6ca01ba959800019d1a9ba30b2996409df938667c718a73d5c794", size = 138380 }, + { url = "https://files.pythonhosted.org/packages/3e/92/53e801988a392f4c1bf6c0dcb6d6ccb247b7988e195e30566e775fe4d44a/bitarray-3.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:15cd52d6dfe4374e31fca6b2d0b50f7ed6a98c316c2befdd2f8be80bb703061c", size = 145240 }, + { url = "https://files.pythonhosted.org/packages/db/7d/0efbbad87455a32f4c23164dfeba452d9348d2157fbd7de720dbbc4f8ae4/bitarray-3.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1c0100d89618f73e7555fe5da8ddffccb9a8f5821fe03560169c31d68d8359ca", size = 144718 }, + { url = "https://files.pythonhosted.org/packages/4a/cb/7cb598e3039394992fe74edf04adb81df89015982ebf7a1064b3278fee21/bitarray-3.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:49770114de14e505f0447680f4daaa0e70c1f60ece113a8ddb3febf7693699a1", size = 141502 }, + { url = "https://files.pythonhosted.org/packages/2a/2c/4f1a7c7a546caeffe23ee719ff89c4dcb7c980b43784db1a1508ce61c2e1/bitarray-3.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:33be8ad210c43536330c6c3a93115311f66a3c0aa2ca1c05b4754f295492da75", size = 307876 }, + { url = "https://files.pythonhosted.org/packages/4d/ab/58b135481a9e25f4e79c6c4589d457cbc16585687a7176807c23b012b100/bitarray-3.5.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c1c90573b55ae88f9fa4385b923138e5b52aec5b13afb48b32254ccc8aeefc46", size = 324416 }, + { url = "https://files.pythonhosted.org/packages/22/01/d8624332a384a2eb027ad8c547be7140ee04e0313feb96923e2c06b28fb0/bitarray-3.5.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:58c7c3763da40369093158eb727caa25187d9d9fa555c4a33efcee98e9a9cc02", size = 316221 }, + { url = "https://files.pythonhosted.org/packages/89/9a/4880127199e131cda284a8a84729ad2723f05c8222d1dbbdd229e78a4595/bitarray-3.5.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c3d0e709566205ea0e91f933aa7bcc4aeeae19093e56f2ba7893dcbe1e85197", size = 309865 }, + { url = "https://files.pythonhosted.org/packages/3a/54/bc35c984289474687951e3d228cff34ef876e1b8c855e4dc022c5dd446e8/bitarray-3.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ac48354c21825bc1737c6568f809e39e5e773220499a533521b8bf1d5edebfb2", size = 297888 }, + { url = "https://files.pythonhosted.org/packages/88/e3/5d0600001d3ad55dd83fee29c5bb3d106f5fb8fcb8a79a701d83f5b65ec5/bitarray-3.5.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:277feb523be1e2a9d3ce7dfb34c46feeadda1956585bd9ab65ac1cf26f3ccb8d", size = 302651 }, + { url = "https://files.pythonhosted.org/packages/db/01/3f10e9a9ab37d642814a6fc97a401923af4399dc11e64ee12d40bbe4d8b8/bitarray-3.5.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:e246f07bae0a91defed1ab79443610953e8f41da423acf81dff5dee0e76c08ee", size = 294947 }, + { url = "https://files.pythonhosted.org/packages/ca/c3/2efe22819c8c4166126835eae712064c60a0ba5825ffb76a9ad695751f4c/bitarray-3.5.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:fb8d5b5e1652ea29fdfa4a453e85626dab729d86880b1f3109f5029e1421abe6", size = 319823 }, + { url = "https://files.pythonhosted.org/packages/86/99/ff57ec6fa89ce746030e90132c52972b6af071b8a97d035b9e15f0154fee/bitarray-3.5.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:8e4f94304e657d20ee9b3455961d1d57f89bd4de77cd866714b5aa2d3d3f7262", size = 322243 }, + { url = "https://files.pythonhosted.org/packages/28/b6/500770a0a9565b64d88c20a79c7f09ee701413316ef2c62b27631c2a8da8/bitarray-3.5.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:be799fdedfc17e7b11dbefc08e5650c1f39dd286b14369b7213b86934f64280b", size = 302037 }, + { url = "https://files.pythonhosted.org/packages/b4/21/46de988c31ed30678b34f381fd1eb81eb1f938604719a689df1a78398604/bitarray-3.5.0-cp39-cp39-win32.whl", hash = "sha256:88a45d6b17b36ee9da05204d88d13e3e660da9c33a94ff447dea0746993a91e7", size = 138091 }, + { url = "https://files.pythonhosted.org/packages/6c/c6/ec5cea02842503fe60d1eb6327d87cb8ba1751d6cee58d98ab2f6d765f22/bitarray-3.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:af2f0f63377ca16629981f2fdf8910dc694848cbd5192ecc912fddef969bf4c2", size = 144630 }, + { url = "https://files.pythonhosted.org/packages/49/4f/1b56a48ee4275c4290097e779ca225a984f4a683d68cbd178ad04833b59a/bitarray-3.5.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:9416cc3077393f2ce9111a2160f7e48ae000503784495493d60e217f519c6f26", size = 139739 }, + { url = "https://files.pythonhosted.org/packages/11/e4/27ab92aa558c2e44aa9f514df038860699520f15c20633298aacfdad0a88/bitarray-3.5.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b2a31d0f44a6bba07fc22841500a8fd9f82affa0ba30cddd18ed8f0370ecf5b8", size = 136666 }, + { url = "https://files.pythonhosted.org/packages/80/c1/b7b545b3eb0ffce1f83a20b46e804dcb42caa4d8b8daf446dea2409fd06f/bitarray-3.5.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:858002d4e0db2760674215ec05c1f3a3390c0b91ccd0f4dd01adba40df953aa2", size = 145357 }, + { url = "https://files.pythonhosted.org/packages/00/7e/93d482572b09b8dd4fe5ec41d16d1e6fbd0efad7810bc2047d0d36268e49/bitarray-3.5.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:625600b045b97569b5ed0c587030b08fba6f567ee3ba8c9fa0fda5f03f4db1e0", size = 146113 }, + { url = "https://files.pythonhosted.org/packages/4a/14/1de63bcb83c99b8c59228733272183f721813573a4b4237fe15603a202ba/bitarray-3.5.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62b32f5040b5be5e6614226d1ff7562798ef127cbd67756c237ac3b2f6ccad91", size = 147662 }, + { url = "https://files.pythonhosted.org/packages/a6/62/fb8b55cdef9bcb07bba7697f58184834ae1082a8eb26538e005a235b31bf/bitarray-3.5.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e031424efbb1f147cf51cfc766d07b57e033d21399a08751c0044325bf5facfb", size = 143533 }, + { url = "https://files.pythonhosted.org/packages/25/dc/b9fabaf6b0f55b64a156179ec5ac2b37bd6c11178eb52d6fb2f5b9407d43/bitarray-3.5.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:6afb05fb457f7fe8db30a20be9c7069a30cfd90c378cd2af12fec01e1e5d6dbd", size = 139790 }, + { url = "https://files.pythonhosted.org/packages/25/d1/efc064d93fc56faf148d49780c3cfe55c121c486fa2dd5ebb78f89372924/bitarray-3.5.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:edb815d21c3df9e9258dce7e8fbc3bca884e2568c389d6ccaee05f6dac1506b9", size = 136783 }, + { url = "https://files.pythonhosted.org/packages/ca/bb/9078bc2952a9be74aeb06ee2b40bfff2f9d7f4be447f29155228e940edb8/bitarray-3.5.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:364ff23c7408a558ead4e09758490c821b11db9f2a1db6e2a6be9679b628bebd", size = 145352 }, + { url = "https://files.pythonhosted.org/packages/6d/72/ccb4307ff8c5dec899307f76adb3e1d7aece384d8fa27eb931ddb8705945/bitarray-3.5.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24c0bac23eca255f19f7861c63fdf0658a9678a2ed083dcb463d37a1b66c4ef8", size = 146098 }, + { url = "https://files.pythonhosted.org/packages/4f/e8/72b1405ee191cc4366518c1ce2391c912897c2c6d7e854aaeebc470a2770/bitarray-3.5.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d20251577809af5bcc67b523639664b92d5e7d685752fe02780d8f6fb2ff7dcc", size = 147570 }, + { url = "https://files.pythonhosted.org/packages/34/7f/d513aca38b293257c307c4435da48256b2870b3871c1c39e475314c8dcba/bitarray-3.5.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:c86759366007584ff463240a9dea326b0b81f42b50001c7bc8f4d270162583bf", size = 143566 }, +] + +[[package]] +name = "cffi" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/07/f44ca684db4e4f08a3fdc6eeb9a0d15dc6883efc7b8c90357fdbf74e186c/cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14", size = 182191 }, + { url = "https://files.pythonhosted.org/packages/08/fd/cc2fedbd887223f9f5d170c96e57cbf655df9831a6546c1727ae13fa977a/cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67", size = 178592 }, + { url = "https://files.pythonhosted.org/packages/de/cc/4635c320081c78d6ffc2cab0a76025b691a91204f4aa317d568ff9280a2d/cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382", size = 426024 }, + { url = "https://files.pythonhosted.org/packages/b6/7b/3b2b250f3aab91abe5f8a51ada1b717935fdaec53f790ad4100fe2ec64d1/cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702", size = 448188 }, + { url = "https://files.pythonhosted.org/packages/d3/48/1b9283ebbf0ec065148d8de05d647a986c5f22586b18120020452fff8f5d/cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3", size = 455571 }, + { url = "https://files.pythonhosted.org/packages/40/87/3b8452525437b40f39ca7ff70276679772ee7e8b394934ff60e63b7b090c/cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6", size = 436687 }, + { url = "https://files.pythonhosted.org/packages/8d/fb/4da72871d177d63649ac449aec2e8a29efe0274035880c7af59101ca2232/cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17", size = 446211 }, + { url = "https://files.pythonhosted.org/packages/ab/a0/62f00bcb411332106c02b663b26f3545a9ef136f80d5df746c05878f8c4b/cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8", size = 461325 }, + { url = "https://files.pythonhosted.org/packages/36/83/76127035ed2e7e27b0787604d99da630ac3123bfb02d8e80c633f218a11d/cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e", size = 438784 }, + { url = "https://files.pythonhosted.org/packages/21/81/a6cd025db2f08ac88b901b745c163d884641909641f9b826e8cb87645942/cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be", size = 461564 }, + { url = "https://files.pythonhosted.org/packages/f8/fe/4d41c2f200c4a457933dbd98d3cf4e911870877bd94d9656cc0fcb390681/cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c", size = 171804 }, + { url = "https://files.pythonhosted.org/packages/d1/b6/0b0f5ab93b0df4acc49cae758c81fe4e5ef26c3ae2e10cc69249dfd8b3ab/cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15", size = 181299 }, + { url = "https://files.pythonhosted.org/packages/6b/f4/927e3a8899e52a27fa57a48607ff7dc91a9ebe97399b357b85a0c7892e00/cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", size = 182264 }, + { url = "https://files.pythonhosted.org/packages/6c/f5/6c3a8efe5f503175aaddcbea6ad0d2c96dad6f5abb205750d1b3df44ef29/cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", size = 178651 }, + { url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259 }, + { url = "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200 }, + { url = "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235 }, + { url = "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", size = 459721 }, + { url = "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", size = 467242 }, + { url = "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999 }, + { url = "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242 }, + { url = "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604 }, + { url = "https://files.pythonhosted.org/packages/34/33/e1b8a1ba29025adbdcda5fb3a36f94c03d771c1b7b12f726ff7fef2ebe36/cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", size = 171727 }, + { url = "https://files.pythonhosted.org/packages/3d/97/50228be003bb2802627d28ec0627837ac0bf35c90cf769812056f235b2d1/cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", size = 181400 }, + { url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178 }, + { url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840 }, + { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803 }, + { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850 }, + { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729 }, + { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256 }, + { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424 }, + { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568 }, + { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736 }, + { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448 }, + { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976 }, + { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989 }, + { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802 }, + { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792 }, + { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893 }, + { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810 }, + { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200 }, + { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447 }, + { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358 }, + { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469 }, + { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475 }, + { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009 }, + { url = "https://files.pythonhosted.org/packages/b9/ea/8bb50596b8ffbc49ddd7a1ad305035daa770202a6b782fc164647c2673ad/cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16", size = 182220 }, + { url = "https://files.pythonhosted.org/packages/ae/11/e77c8cd24f58285a82c23af484cf5b124a376b32644e445960d1a4654c3a/cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36", size = 178605 }, + { url = "https://files.pythonhosted.org/packages/ed/65/25a8dc32c53bf5b7b6c2686b42ae2ad58743f7ff644844af7cdb29b49361/cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8", size = 424910 }, + { url = "https://files.pythonhosted.org/packages/42/7a/9d086fab7c66bd7c4d0f27c57a1b6b068ced810afc498cc8c49e0088661c/cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576", size = 447200 }, + { url = "https://files.pythonhosted.org/packages/da/63/1785ced118ce92a993b0ec9e0d0ac8dc3e5dbfbcaa81135be56c69cabbb6/cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87", size = 454565 }, + { url = "https://files.pythonhosted.org/packages/74/06/90b8a44abf3556599cdec107f7290277ae8901a58f75e6fe8f970cd72418/cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0", size = 435635 }, + { url = "https://files.pythonhosted.org/packages/bd/62/a1f468e5708a70b1d86ead5bab5520861d9c7eacce4a885ded9faa7729c3/cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3", size = 445218 }, + { url = "https://files.pythonhosted.org/packages/5b/95/b34462f3ccb09c2594aa782d90a90b045de4ff1f70148ee79c69d37a0a5a/cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595", size = 460486 }, + { url = "https://files.pythonhosted.org/packages/fc/fc/a1e4bebd8d680febd29cf6c8a40067182b64f00c7d105f8f26b5bc54317b/cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a", size = 437911 }, + { url = "https://files.pythonhosted.org/packages/e6/c3/21cab7a6154b6a5ea330ae80de386e7665254835b9e98ecc1340b3a7de9a/cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e", size = 460632 }, + { url = "https://files.pythonhosted.org/packages/cb/b5/fd9f8b5a84010ca169ee49f4e4ad6f8c05f4e3545b72ee041dbbcb159882/cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7", size = 171820 }, + { url = "https://files.pythonhosted.org/packages/8c/52/b08750ce0bce45c143e1b5d7357ee8c55341b52bdef4b0f081af1eb248c2/cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662", size = 181290 }, +] + +[[package]] +name = "contourpy" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/f6/31a8f28b4a2a4fa0e01085e542f3081ab0588eff8e589d39d775172c9792/contourpy-1.3.0.tar.gz", hash = "sha256:7ffa0db17717a8ffb127efd0c95a4362d996b892c2904db72428d5b52e1938a4", size = 13464370 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6c/e0/be8dcc796cfdd96708933e0e2da99ba4bb8f9b2caa9d560a50f3f09a65f3/contourpy-1.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:880ea32e5c774634f9fcd46504bf9f080a41ad855f4fef54f5380f5133d343c7", size = 265366 }, + { url = "https://files.pythonhosted.org/packages/50/d6/c953b400219443535d412fcbbc42e7a5e823291236bc0bb88936e3cc9317/contourpy-1.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:76c905ef940a4474a6289c71d53122a4f77766eef23c03cd57016ce19d0f7b42", size = 249226 }, + { url = "https://files.pythonhosted.org/packages/6f/b4/6fffdf213ffccc28483c524b9dad46bb78332851133b36ad354b856ddc7c/contourpy-1.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:92f8557cbb07415a4d6fa191f20fd9d2d9eb9c0b61d1b2f52a8926e43c6e9af7", size = 308460 }, + { url = "https://files.pythonhosted.org/packages/cf/6c/118fc917b4050f0afe07179a6dcbe4f3f4ec69b94f36c9e128c4af480fb8/contourpy-1.3.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:36f965570cff02b874773c49bfe85562b47030805d7d8360748f3eca570f4cab", size = 347623 }, + { url = "https://files.pythonhosted.org/packages/f9/a4/30ff110a81bfe3abf7b9673284d21ddce8cc1278f6f77393c91199da4c90/contourpy-1.3.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cacd81e2d4b6f89c9f8a5b69b86490152ff39afc58a95af002a398273e5ce589", size = 317761 }, + { url = "https://files.pythonhosted.org/packages/99/e6/d11966962b1aa515f5586d3907ad019f4b812c04e4546cc19ebf62b5178e/contourpy-1.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:69375194457ad0fad3a839b9e29aa0b0ed53bb54db1bfb6c3ae43d111c31ce41", size = 322015 }, + { url = "https://files.pythonhosted.org/packages/4d/e3/182383743751d22b7b59c3c753277b6aee3637049197624f333dac5b4c80/contourpy-1.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a52040312b1a858b5e31ef28c2e865376a386c60c0e248370bbea2d3f3b760d", size = 1262672 }, + { url = "https://files.pythonhosted.org/packages/78/53/974400c815b2e605f252c8fb9297e2204347d1755a5374354ee77b1ea259/contourpy-1.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3faeb2998e4fcb256542e8a926d08da08977f7f5e62cf733f3c211c2a5586223", size = 1321688 }, + { url = "https://files.pythonhosted.org/packages/52/29/99f849faed5593b2926a68a31882af98afbeac39c7fdf7de491d9c85ec6a/contourpy-1.3.0-cp310-cp310-win32.whl", hash = "sha256:36e0cff201bcb17a0a8ecc7f454fe078437fa6bda730e695a92f2d9932bd507f", size = 171145 }, + { url = "https://files.pythonhosted.org/packages/a9/97/3f89bba79ff6ff2b07a3cbc40aa693c360d5efa90d66e914f0ff03b95ec7/contourpy-1.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:87ddffef1dbe5e669b5c2440b643d3fdd8622a348fe1983fad7a0f0ccb1cd67b", size = 216019 }, + { url = "https://files.pythonhosted.org/packages/b3/1f/9375917786cb39270b0ee6634536c0e22abf225825602688990d8f5c6c19/contourpy-1.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0fa4c02abe6c446ba70d96ece336e621efa4aecae43eaa9b030ae5fb92b309ad", size = 266356 }, + { url = "https://files.pythonhosted.org/packages/05/46/9256dd162ea52790c127cb58cfc3b9e3413a6e3478917d1f811d420772ec/contourpy-1.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:834e0cfe17ba12f79963861e0f908556b2cedd52e1f75e6578801febcc6a9f49", size = 250915 }, + { url = "https://files.pythonhosted.org/packages/e1/5d/3056c167fa4486900dfbd7e26a2fdc2338dc58eee36d490a0ed3ddda5ded/contourpy-1.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dbc4c3217eee163fa3984fd1567632b48d6dfd29216da3ded3d7b844a8014a66", size = 310443 }, + { url = "https://files.pythonhosted.org/packages/ca/c2/1a612e475492e07f11c8e267ea5ec1ce0d89971be496c195e27afa97e14a/contourpy-1.3.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4865cd1d419e0c7a7bf6de1777b185eebdc51470800a9f42b9e9decf17762081", size = 348548 }, + { url = "https://files.pythonhosted.org/packages/45/cf/2c2fc6bb5874158277b4faf136847f0689e1b1a1f640a36d76d52e78907c/contourpy-1.3.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:303c252947ab4b14c08afeb52375b26781ccd6a5ccd81abcdfc1fafd14cf93c1", size = 319118 }, + { url = "https://files.pythonhosted.org/packages/03/33/003065374f38894cdf1040cef474ad0546368eea7e3a51d48b8a423961f8/contourpy-1.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:637f674226be46f6ba372fd29d9523dd977a291f66ab2a74fbeb5530bb3f445d", size = 323162 }, + { url = "https://files.pythonhosted.org/packages/42/80/e637326e85e4105a802e42959f56cff2cd39a6b5ef68d5d9aee3ea5f0e4c/contourpy-1.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:76a896b2f195b57db25d6b44e7e03f221d32fe318d03ede41f8b4d9ba1bff53c", size = 1265396 }, + { url = "https://files.pythonhosted.org/packages/7c/3b/8cbd6416ca1bbc0202b50f9c13b2e0b922b64be888f9d9ee88e6cfabfb51/contourpy-1.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e1fd23e9d01591bab45546c089ae89d926917a66dceb3abcf01f6105d927e2cb", size = 1324297 }, + { url = "https://files.pythonhosted.org/packages/4d/2c/021a7afaa52fe891f25535506cc861c30c3c4e5a1c1ce94215e04b293e72/contourpy-1.3.0-cp311-cp311-win32.whl", hash = "sha256:d402880b84df3bec6eab53cd0cf802cae6a2ef9537e70cf75e91618a3801c20c", size = 171808 }, + { url = "https://files.pythonhosted.org/packages/8d/2f/804f02ff30a7fae21f98198828d0857439ec4c91a96e20cf2d6c49372966/contourpy-1.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:6cb6cc968059db9c62cb35fbf70248f40994dfcd7aa10444bbf8b3faeb7c2d67", size = 217181 }, + { url = "https://files.pythonhosted.org/packages/c9/92/8e0bbfe6b70c0e2d3d81272b58c98ac69ff1a4329f18c73bd64824d8b12e/contourpy-1.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:570ef7cf892f0afbe5b2ee410c507ce12e15a5fa91017a0009f79f7d93a1268f", size = 267838 }, + { url = "https://files.pythonhosted.org/packages/e3/04/33351c5d5108460a8ce6d512307690b023f0cfcad5899499f5c83b9d63b1/contourpy-1.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:da84c537cb8b97d153e9fb208c221c45605f73147bd4cadd23bdae915042aad6", size = 251549 }, + { url = "https://files.pythonhosted.org/packages/51/3d/aa0fe6ae67e3ef9f178389e4caaaa68daf2f9024092aa3c6032e3d174670/contourpy-1.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0be4d8425bfa755e0fd76ee1e019636ccc7c29f77a7c86b4328a9eb6a26d0639", size = 303177 }, + { url = "https://files.pythonhosted.org/packages/56/c3/c85a7e3e0cab635575d3b657f9535443a6f5d20fac1a1911eaa4bbe1aceb/contourpy-1.3.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9c0da700bf58f6e0b65312d0a5e695179a71d0163957fa381bb3c1f72972537c", size = 341735 }, + { url = "https://files.pythonhosted.org/packages/dd/8d/20f7a211a7be966a53f474bc90b1a8202e9844b3f1ef85f3ae45a77151ee/contourpy-1.3.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eb8b141bb00fa977d9122636b16aa67d37fd40a3d8b52dd837e536d64b9a4d06", size = 314679 }, + { url = "https://files.pythonhosted.org/packages/6e/be/524e377567defac0e21a46e2a529652d165fed130a0d8a863219303cee18/contourpy-1.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3634b5385c6716c258d0419c46d05c8aa7dc8cb70326c9a4fb66b69ad2b52e09", size = 320549 }, + { url = "https://files.pythonhosted.org/packages/0f/96/fdb2552a172942d888915f3a6663812e9bc3d359d53dafd4289a0fb462f0/contourpy-1.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0dce35502151b6bd35027ac39ba6e5a44be13a68f55735c3612c568cac3805fd", size = 1263068 }, + { url = "https://files.pythonhosted.org/packages/2a/25/632eab595e3140adfa92f1322bf8915f68c932bac468e89eae9974cf1c00/contourpy-1.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:aea348f053c645100612b333adc5983d87be69acdc6d77d3169c090d3b01dc35", size = 1322833 }, + { url = "https://files.pythonhosted.org/packages/73/e3/69738782e315a1d26d29d71a550dbbe3eb6c653b028b150f70c1a5f4f229/contourpy-1.3.0-cp312-cp312-win32.whl", hash = "sha256:90f73a5116ad1ba7174341ef3ea5c3150ddf20b024b98fb0c3b29034752c8aeb", size = 172681 }, + { url = "https://files.pythonhosted.org/packages/0c/89/9830ba00d88e43d15e53d64931e66b8792b46eb25e2050a88fec4a0df3d5/contourpy-1.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:b11b39aea6be6764f84360fce6c82211a9db32a7c7de8fa6dd5397cf1d079c3b", size = 218283 }, + { url = "https://files.pythonhosted.org/packages/53/a1/d20415febfb2267af2d7f06338e82171824d08614084714fb2c1dac9901f/contourpy-1.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3e1c7fa44aaae40a2247e2e8e0627f4bea3dd257014764aa644f319a5f8600e3", size = 267879 }, + { url = "https://files.pythonhosted.org/packages/aa/45/5a28a3570ff6218d8bdfc291a272a20d2648104815f01f0177d103d985e1/contourpy-1.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:364174c2a76057feef647c802652f00953b575723062560498dc7930fc9b1cb7", size = 251573 }, + { url = "https://files.pythonhosted.org/packages/39/1c/d3f51540108e3affa84f095c8b04f0aa833bb797bc8baa218a952a98117d/contourpy-1.3.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32b238b3b3b649e09ce9aaf51f0c261d38644bdfa35cbaf7b263457850957a84", size = 303184 }, + { url = "https://files.pythonhosted.org/packages/00/56/1348a44fb6c3a558c1a3a0cd23d329d604c99d81bf5a4b58c6b71aab328f/contourpy-1.3.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d51fca85f9f7ad0b65b4b9fe800406d0d77017d7270d31ec3fb1cc07358fdea0", size = 340262 }, + { url = "https://files.pythonhosted.org/packages/2b/23/00d665ba67e1bb666152131da07e0f24c95c3632d7722caa97fb61470eca/contourpy-1.3.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:732896af21716b29ab3e988d4ce14bc5133733b85956316fb0c56355f398099b", size = 313806 }, + { url = "https://files.pythonhosted.org/packages/5a/42/3cf40f7040bb8362aea19af9a5fb7b32ce420f645dd1590edcee2c657cd5/contourpy-1.3.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d73f659398a0904e125280836ae6f88ba9b178b2fed6884f3b1f95b989d2c8da", size = 319710 }, + { url = "https://files.pythonhosted.org/packages/05/32/f3bfa3fc083b25e1a7ae09197f897476ee68e7386e10404bdf9aac7391f0/contourpy-1.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c6c7c2408b7048082932cf4e641fa3b8ca848259212f51c8c59c45aa7ac18f14", size = 1264107 }, + { url = "https://files.pythonhosted.org/packages/1c/1e/1019d34473a736664f2439542b890b2dc4c6245f5c0d8cdfc0ccc2cab80c/contourpy-1.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f317576606de89da6b7e0861cf6061f6146ead3528acabff9236458a6ba467f8", size = 1322458 }, + { url = "https://files.pythonhosted.org/packages/22/85/4f8bfd83972cf8909a4d36d16b177f7b8bdd942178ea4bf877d4a380a91c/contourpy-1.3.0-cp313-cp313-win32.whl", hash = "sha256:31cd3a85dbdf1fc002280c65caa7e2b5f65e4a973fcdf70dd2fdcb9868069294", size = 172643 }, + { url = "https://files.pythonhosted.org/packages/cc/4a/fb3c83c1baba64ba90443626c228ca14f19a87c51975d3b1de308dd2cf08/contourpy-1.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:4553c421929ec95fb07b3aaca0fae668b2eb5a5203d1217ca7c34c063c53d087", size = 218301 }, + { url = "https://files.pythonhosted.org/packages/76/65/702f4064f397821fea0cb493f7d3bc95a5d703e20954dce7d6d39bacf378/contourpy-1.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:345af746d7766821d05d72cb8f3845dfd08dd137101a2cb9b24de277d716def8", size = 278972 }, + { url = "https://files.pythonhosted.org/packages/80/85/21f5bba56dba75c10a45ec00ad3b8190dbac7fd9a8a8c46c6116c933e9cf/contourpy-1.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3bb3808858a9dc68f6f03d319acd5f1b8a337e6cdda197f02f4b8ff67ad2057b", size = 263375 }, + { url = "https://files.pythonhosted.org/packages/0a/64/084c86ab71d43149f91ab3a4054ccf18565f0a8af36abfa92b1467813ed6/contourpy-1.3.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:420d39daa61aab1221567b42eecb01112908b2cab7f1b4106a52caaec8d36973", size = 307188 }, + { url = "https://files.pythonhosted.org/packages/3d/ff/d61a4c288dc42da0084b8d9dc2aa219a850767165d7d9a9c364ff530b509/contourpy-1.3.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4d63ee447261e963af02642ffcb864e5a2ee4cbfd78080657a9880b8b1868e18", size = 345644 }, + { url = "https://files.pythonhosted.org/packages/ca/aa/00d2313d35ec03f188e8f0786c2fc61f589306e02fdc158233697546fd58/contourpy-1.3.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:167d6c890815e1dac9536dca00828b445d5d0df4d6a8c6adb4a7ec3166812fa8", size = 317141 }, + { url = "https://files.pythonhosted.org/packages/8d/6a/b5242c8cb32d87f6abf4f5e3044ca397cb1a76712e3fa2424772e3ff495f/contourpy-1.3.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:710a26b3dc80c0e4febf04555de66f5fd17e9cf7170a7b08000601a10570bda6", size = 323469 }, + { url = "https://files.pythonhosted.org/packages/6f/a6/73e929d43028a9079aca4bde107494864d54f0d72d9db508a51ff0878593/contourpy-1.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:75ee7cb1a14c617f34a51d11fa7524173e56551646828353c4af859c56b766e2", size = 1260894 }, + { url = "https://files.pythonhosted.org/packages/2b/1e/1e726ba66eddf21c940821df8cf1a7d15cb165f0682d62161eaa5e93dae1/contourpy-1.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:33c92cdae89ec5135d036e7218e69b0bb2851206077251f04a6c4e0e21f03927", size = 1314829 }, + { url = "https://files.pythonhosted.org/packages/b3/e3/b9f72758adb6ef7397327ceb8b9c39c75711affb220e4f53c745ea1d5a9a/contourpy-1.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a11077e395f67ffc2c44ec2418cfebed032cd6da3022a94fc227b6faf8e2acb8", size = 265518 }, + { url = "https://files.pythonhosted.org/packages/ec/22/19f5b948367ab5260fb41d842c7a78dae645603881ea6bc39738bcfcabf6/contourpy-1.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e8134301d7e204c88ed7ab50028ba06c683000040ede1d617298611f9dc6240c", size = 249350 }, + { url = "https://files.pythonhosted.org/packages/26/76/0c7d43263dd00ae21a91a24381b7e813d286a3294d95d179ef3a7b9fb1d7/contourpy-1.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e12968fdfd5bb45ffdf6192a590bd8ddd3ba9e58360b29683c6bb71a7b41edca", size = 309167 }, + { url = "https://files.pythonhosted.org/packages/96/3b/cadff6773e89f2a5a492c1a8068e21d3fccaf1a1c1df7d65e7c8e3ef60ba/contourpy-1.3.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fd2a0fc506eccaaa7595b7e1418951f213cf8255be2600f1ea1b61e46a60c55f", size = 348279 }, + { url = "https://files.pythonhosted.org/packages/e1/86/158cc43aa549d2081a955ab11c6bdccc7a22caacc2af93186d26f5f48746/contourpy-1.3.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4cfb5c62ce023dfc410d6059c936dcf96442ba40814aefbfa575425a3a7f19dc", size = 318519 }, + { url = "https://files.pythonhosted.org/packages/05/11/57335544a3027e9b96a05948c32e566328e3a2f84b7b99a325b7a06d2b06/contourpy-1.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68a32389b06b82c2fdd68276148d7b9275b5f5cf13e5417e4252f6d1a34f72a2", size = 321922 }, + { url = "https://files.pythonhosted.org/packages/0b/e3/02114f96543f4a1b694333b92a6dcd4f8eebbefcc3a5f3bbb1316634178f/contourpy-1.3.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:94e848a6b83da10898cbf1311a815f770acc9b6a3f2d646f330d57eb4e87592e", size = 1258017 }, + { url = "https://files.pythonhosted.org/packages/f3/3b/bfe4c81c6d5881c1c643dde6620be0b42bf8aab155976dd644595cfab95c/contourpy-1.3.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:d78ab28a03c854a873787a0a42254a0ccb3cb133c672f645c9f9c8f3ae9d0800", size = 1316773 }, + { url = "https://files.pythonhosted.org/packages/f1/17/c52d2970784383cafb0bd918b6fb036d98d96bbf0bc1befb5d1e31a07a70/contourpy-1.3.0-cp39-cp39-win32.whl", hash = "sha256:81cb5ed4952aae6014bc9d0421dec7c5835c9c8c31cdf51910b708f548cf58e5", size = 171353 }, + { url = "https://files.pythonhosted.org/packages/53/23/db9f69676308e094d3c45f20cc52e12d10d64f027541c995d89c11ad5c75/contourpy-1.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:14e262f67bd7e6eb6880bc564dcda30b15e351a594657e55b7eec94b6ef72843", size = 211817 }, + { url = "https://files.pythonhosted.org/packages/d1/09/60e486dc2b64c94ed33e58dcfb6f808192c03dfc5574c016218b9b7680dc/contourpy-1.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:fe41b41505a5a33aeaed2a613dccaeaa74e0e3ead6dd6fd3a118fb471644fd6c", size = 261886 }, + { url = "https://files.pythonhosted.org/packages/19/20/b57f9f7174fcd439a7789fb47d764974ab646fa34d1790551de386457a8e/contourpy-1.3.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eca7e17a65f72a5133bdbec9ecf22401c62bcf4821361ef7811faee695799779", size = 311008 }, + { url = "https://files.pythonhosted.org/packages/74/fc/5040d42623a1845d4f17a418e590fd7a79ae8cb2bad2b2f83de63c3bdca4/contourpy-1.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:1ec4dc6bf570f5b22ed0d7efba0dfa9c5b9e0431aeea7581aa217542d9e809a4", size = 215690 }, + { url = "https://files.pythonhosted.org/packages/2b/24/dc3dcd77ac7460ab7e9d2b01a618cb31406902e50e605a8d6091f0a8f7cc/contourpy-1.3.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:00ccd0dbaad6d804ab259820fa7cb0b8036bda0686ef844d24125d8287178ce0", size = 261894 }, + { url = "https://files.pythonhosted.org/packages/b1/db/531642a01cfec39d1682e46b5457b07cf805e3c3c584ec27e2a6223f8f6c/contourpy-1.3.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ca947601224119117f7c19c9cdf6b3ab54c5726ef1d906aa4a69dfb6dd58102", size = 311099 }, + { url = "https://files.pythonhosted.org/packages/38/1e/94bda024d629f254143a134eead69e21c836429a2a6ce82209a00ddcb79a/contourpy-1.3.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:c6ec93afeb848a0845a18989da3beca3eec2c0f852322efe21af1931147d12cb", size = 215838 }, +] + +[[package]] +name = "contourpy" +version = "1.3.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/54/eb9bfc647b19f2009dd5c7f5ec51c4e6ca831725f1aea7a993034f483147/contourpy-1.3.2.tar.gz", hash = "sha256:b6945942715a034c671b7fc54f9588126b0b8bf23db2696e3ca8328f3ff0ab54", size = 13466130 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/a3/da4153ec8fe25d263aa48c1a4cbde7f49b59af86f0b6f7862788c60da737/contourpy-1.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ba38e3f9f330af820c4b27ceb4b9c7feee5fe0493ea53a8720f4792667465934", size = 268551 }, + { url = "https://files.pythonhosted.org/packages/2f/6c/330de89ae1087eb622bfca0177d32a7ece50c3ef07b28002de4757d9d875/contourpy-1.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dc41ba0714aa2968d1f8674ec97504a8f7e334f48eeacebcaa6256213acb0989", size = 253399 }, + { url = "https://files.pythonhosted.org/packages/c1/bd/20c6726b1b7f81a8bee5271bed5c165f0a8e1f572578a9d27e2ccb763cb2/contourpy-1.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9be002b31c558d1ddf1b9b415b162c603405414bacd6932d031c5b5a8b757f0d", size = 312061 }, + { url = "https://files.pythonhosted.org/packages/22/fc/a9665c88f8a2473f823cf1ec601de9e5375050f1958cbb356cdf06ef1ab6/contourpy-1.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8d2e74acbcba3bfdb6d9d8384cdc4f9260cae86ed9beee8bd5f54fee49a430b9", size = 351956 }, + { url = "https://files.pythonhosted.org/packages/25/eb/9f0a0238f305ad8fb7ef42481020d6e20cf15e46be99a1fcf939546a177e/contourpy-1.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e259bced5549ac64410162adc973c5e2fb77f04df4a439d00b478e57a0e65512", size = 320872 }, + { url = "https://files.pythonhosted.org/packages/32/5c/1ee32d1c7956923202f00cf8d2a14a62ed7517bdc0ee1e55301227fc273c/contourpy-1.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad687a04bc802cbe8b9c399c07162a3c35e227e2daccf1668eb1f278cb698631", size = 325027 }, + { url = "https://files.pythonhosted.org/packages/83/bf/9baed89785ba743ef329c2b07fd0611d12bfecbedbdd3eeecf929d8d3b52/contourpy-1.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cdd22595308f53ef2f891040ab2b93d79192513ffccbd7fe19be7aa773a5e09f", size = 1306641 }, + { url = "https://files.pythonhosted.org/packages/d4/cc/74e5e83d1e35de2d28bd97033426b450bc4fd96e092a1f7a63dc7369b55d/contourpy-1.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b4f54d6a2defe9f257327b0f243612dd051cc43825587520b1bf74a31e2f6ef2", size = 1374075 }, + { url = "https://files.pythonhosted.org/packages/0c/42/17f3b798fd5e033b46a16f8d9fcb39f1aba051307f5ebf441bad1ecf78f8/contourpy-1.3.2-cp310-cp310-win32.whl", hash = "sha256:f939a054192ddc596e031e50bb13b657ce318cf13d264f095ce9db7dc6ae81c0", size = 177534 }, + { url = "https://files.pythonhosted.org/packages/54/ec/5162b8582f2c994721018d0c9ece9dc6ff769d298a8ac6b6a652c307e7df/contourpy-1.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:c440093bbc8fc21c637c03bafcbef95ccd963bc6e0514ad887932c18ca2a759a", size = 221188 }, + { url = "https://files.pythonhosted.org/packages/b3/b9/ede788a0b56fc5b071639d06c33cb893f68b1178938f3425debebe2dab78/contourpy-1.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6a37a2fb93d4df3fc4c0e363ea4d16f83195fc09c891bc8ce072b9d084853445", size = 269636 }, + { url = "https://files.pythonhosted.org/packages/e6/75/3469f011d64b8bbfa04f709bfc23e1dd71be54d05b1b083be9f5b22750d1/contourpy-1.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b7cd50c38f500bbcc9b6a46643a40e0913673f869315d8e70de0438817cb7773", size = 254636 }, + { url = "https://files.pythonhosted.org/packages/8d/2f/95adb8dae08ce0ebca4fd8e7ad653159565d9739128b2d5977806656fcd2/contourpy-1.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6658ccc7251a4433eebd89ed2672c2ed96fba367fd25ca9512aa92a4b46c4f1", size = 313053 }, + { url = "https://files.pythonhosted.org/packages/c3/a6/8ccf97a50f31adfa36917707fe39c9a0cbc24b3bbb58185577f119736cc9/contourpy-1.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:70771a461aaeb335df14deb6c97439973d253ae70660ca085eec25241137ef43", size = 352985 }, + { url = "https://files.pythonhosted.org/packages/1d/b6/7925ab9b77386143f39d9c3243fdd101621b4532eb126743201160ffa7e6/contourpy-1.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65a887a6e8c4cd0897507d814b14c54a8c2e2aa4ac9f7686292f9769fcf9a6ab", size = 323750 }, + { url = "https://files.pythonhosted.org/packages/c2/f3/20c5d1ef4f4748e52d60771b8560cf00b69d5c6368b5c2e9311bcfa2a08b/contourpy-1.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3859783aefa2b8355697f16642695a5b9792e7a46ab86da1118a4a23a51a33d7", size = 326246 }, + { url = "https://files.pythonhosted.org/packages/8c/e5/9dae809e7e0b2d9d70c52b3d24cba134dd3dad979eb3e5e71f5df22ed1f5/contourpy-1.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:eab0f6db315fa4d70f1d8ab514e527f0366ec021ff853d7ed6a2d33605cf4b83", size = 1308728 }, + { url = "https://files.pythonhosted.org/packages/e2/4a/0058ba34aeea35c0b442ae61a4f4d4ca84d6df8f91309bc2d43bb8dd248f/contourpy-1.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d91a3ccc7fea94ca0acab82ceb77f396d50a1f67412efe4c526f5d20264e6ecd", size = 1375762 }, + { url = "https://files.pythonhosted.org/packages/09/33/7174bdfc8b7767ef2c08ed81244762d93d5c579336fc0b51ca57b33d1b80/contourpy-1.3.2-cp311-cp311-win32.whl", hash = "sha256:1c48188778d4d2f3d48e4643fb15d8608b1d01e4b4d6b0548d9b336c28fc9b6f", size = 178196 }, + { url = "https://files.pythonhosted.org/packages/5e/fe/4029038b4e1c4485cef18e480b0e2cd2d755448bb071eb9977caac80b77b/contourpy-1.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:5ebac872ba09cb8f2131c46b8739a7ff71de28a24c869bcad554477eb089a878", size = 222017 }, + { url = "https://files.pythonhosted.org/packages/34/f7/44785876384eff370c251d58fd65f6ad7f39adce4a093c934d4a67a7c6b6/contourpy-1.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4caf2bcd2969402bf77edc4cb6034c7dd7c0803213b3523f111eb7460a51b8d2", size = 271580 }, + { url = "https://files.pythonhosted.org/packages/93/3b/0004767622a9826ea3d95f0e9d98cd8729015768075d61f9fea8eeca42a8/contourpy-1.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:82199cb78276249796419fe36b7386bd8d2cc3f28b3bc19fe2454fe2e26c4c15", size = 255530 }, + { url = "https://files.pythonhosted.org/packages/e7/bb/7bd49e1f4fa805772d9fd130e0d375554ebc771ed7172f48dfcd4ca61549/contourpy-1.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:106fab697af11456fcba3e352ad50effe493a90f893fca6c2ca5c033820cea92", size = 307688 }, + { url = "https://files.pythonhosted.org/packages/fc/97/e1d5dbbfa170725ef78357a9a0edc996b09ae4af170927ba8ce977e60a5f/contourpy-1.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d14f12932a8d620e307f715857107b1d1845cc44fdb5da2bc8e850f5ceba9f87", size = 347331 }, + { url = "https://files.pythonhosted.org/packages/6f/66/e69e6e904f5ecf6901be3dd16e7e54d41b6ec6ae3405a535286d4418ffb4/contourpy-1.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:532fd26e715560721bb0d5fc7610fce279b3699b018600ab999d1be895b09415", size = 318963 }, + { url = "https://files.pythonhosted.org/packages/a8/32/b8a1c8965e4f72482ff2d1ac2cd670ce0b542f203c8e1d34e7c3e6925da7/contourpy-1.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26b383144cf2d2c29f01a1e8170f50dacf0eac02d64139dcd709a8ac4eb3cfe", size = 323681 }, + { url = "https://files.pythonhosted.org/packages/30/c6/12a7e6811d08757c7162a541ca4c5c6a34c0f4e98ef2b338791093518e40/contourpy-1.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c49f73e61f1f774650a55d221803b101d966ca0c5a2d6d5e4320ec3997489441", size = 1308674 }, + { url = "https://files.pythonhosted.org/packages/2a/8a/bebe5a3f68b484d3a2b8ffaf84704b3e343ef1addea528132ef148e22b3b/contourpy-1.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3d80b2c0300583228ac98d0a927a1ba6a2ba6b8a742463c564f1d419ee5b211e", size = 1380480 }, + { url = "https://files.pythonhosted.org/packages/34/db/fcd325f19b5978fb509a7d55e06d99f5f856294c1991097534360b307cf1/contourpy-1.3.2-cp312-cp312-win32.whl", hash = "sha256:90df94c89a91b7362e1142cbee7568f86514412ab8a2c0d0fca72d7e91b62912", size = 178489 }, + { url = "https://files.pythonhosted.org/packages/01/c8/fadd0b92ffa7b5eb5949bf340a63a4a496a6930a6c37a7ba0f12acb076d6/contourpy-1.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:8c942a01d9163e2e5cfb05cb66110121b8d07ad438a17f9e766317bcb62abf73", size = 223042 }, + { url = "https://files.pythonhosted.org/packages/2e/61/5673f7e364b31e4e7ef6f61a4b5121c5f170f941895912f773d95270f3a2/contourpy-1.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:de39db2604ae755316cb5967728f4bea92685884b1e767b7c24e983ef5f771cb", size = 271630 }, + { url = "https://files.pythonhosted.org/packages/ff/66/a40badddd1223822c95798c55292844b7e871e50f6bfd9f158cb25e0bd39/contourpy-1.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3f9e896f447c5c8618f1edb2bafa9a4030f22a575ec418ad70611450720b5b08", size = 255670 }, + { url = "https://files.pythonhosted.org/packages/1e/c7/cf9fdee8200805c9bc3b148f49cb9482a4e3ea2719e772602a425c9b09f8/contourpy-1.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71e2bd4a1c4188f5c2b8d274da78faab884b59df20df63c34f74aa1813c4427c", size = 306694 }, + { url = "https://files.pythonhosted.org/packages/dd/e7/ccb9bec80e1ba121efbffad7f38021021cda5be87532ec16fd96533bb2e0/contourpy-1.3.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de425af81b6cea33101ae95ece1f696af39446db9682a0b56daaa48cfc29f38f", size = 345986 }, + { url = "https://files.pythonhosted.org/packages/dc/49/ca13bb2da90391fa4219fdb23b078d6065ada886658ac7818e5441448b78/contourpy-1.3.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:977e98a0e0480d3fe292246417239d2d45435904afd6d7332d8455981c408b85", size = 318060 }, + { url = "https://files.pythonhosted.org/packages/c8/65/5245ce8c548a8422236c13ffcdcdada6a2a812c361e9e0c70548bb40b661/contourpy-1.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:434f0adf84911c924519d2b08fc10491dd282b20bdd3fa8f60fd816ea0b48841", size = 322747 }, + { url = "https://files.pythonhosted.org/packages/72/30/669b8eb48e0a01c660ead3752a25b44fdb2e5ebc13a55782f639170772f9/contourpy-1.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c66c4906cdbc50e9cba65978823e6e00b45682eb09adbb78c9775b74eb222422", size = 1308895 }, + { url = "https://files.pythonhosted.org/packages/05/5a/b569f4250decee6e8d54498be7bdf29021a4c256e77fe8138c8319ef8eb3/contourpy-1.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8b7fc0cd78ba2f4695fd0a6ad81a19e7e3ab825c31b577f384aa9d7817dc3bef", size = 1379098 }, + { url = "https://files.pythonhosted.org/packages/19/ba/b227c3886d120e60e41b28740ac3617b2f2b971b9f601c835661194579f1/contourpy-1.3.2-cp313-cp313-win32.whl", hash = "sha256:15ce6ab60957ca74cff444fe66d9045c1fd3e92c8936894ebd1f3eef2fff075f", size = 178535 }, + { url = "https://files.pythonhosted.org/packages/12/6e/2fed56cd47ca739b43e892707ae9a13790a486a3173be063681ca67d2262/contourpy-1.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:e1578f7eafce927b168752ed7e22646dad6cd9bca673c60bff55889fa236ebf9", size = 223096 }, + { url = "https://files.pythonhosted.org/packages/54/4c/e76fe2a03014a7c767d79ea35c86a747e9325537a8b7627e0e5b3ba266b4/contourpy-1.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0475b1f6604896bc7c53bb070e355e9321e1bc0d381735421a2d2068ec56531f", size = 285090 }, + { url = "https://files.pythonhosted.org/packages/7b/e2/5aba47debd55d668e00baf9651b721e7733975dc9fc27264a62b0dd26eb8/contourpy-1.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c85bb486e9be652314bb5b9e2e3b0d1b2e643d5eec4992c0fbe8ac71775da739", size = 268643 }, + { url = "https://files.pythonhosted.org/packages/a1/37/cd45f1f051fe6230f751cc5cdd2728bb3a203f5619510ef11e732109593c/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:745b57db7758f3ffc05a10254edd3182a2a83402a89c00957a8e8a22f5582823", size = 310443 }, + { url = "https://files.pythonhosted.org/packages/8b/a2/36ea6140c306c9ff6dd38e3bcec80b3b018474ef4d17eb68ceecd26675f4/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:970e9173dbd7eba9b4e01aab19215a48ee5dd3f43cef736eebde064a171f89a5", size = 349865 }, + { url = "https://files.pythonhosted.org/packages/95/b7/2fc76bc539693180488f7b6cc518da7acbbb9e3b931fd9280504128bf956/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6c4639a9c22230276b7bffb6a850dfc8258a2521305e1faefe804d006b2e532", size = 321162 }, + { url = "https://files.pythonhosted.org/packages/f4/10/76d4f778458b0aa83f96e59d65ece72a060bacb20cfbee46cf6cd5ceba41/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc829960f34ba36aad4302e78eabf3ef16a3a100863f0d4eeddf30e8a485a03b", size = 327355 }, + { url = "https://files.pythonhosted.org/packages/43/a3/10cf483ea683f9f8ab096c24bad3cce20e0d1dd9a4baa0e2093c1c962d9d/contourpy-1.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d32530b534e986374fc19eaa77fcb87e8a99e5431499949b828312bdcd20ac52", size = 1307935 }, + { url = "https://files.pythonhosted.org/packages/78/73/69dd9a024444489e22d86108e7b913f3528f56cfc312b5c5727a44188471/contourpy-1.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e298e7e70cf4eb179cc1077be1c725b5fd131ebc81181bf0c03525c8abc297fd", size = 1372168 }, + { url = "https://files.pythonhosted.org/packages/0f/1b/96d586ccf1b1a9d2004dd519b25fbf104a11589abfd05484ff12199cca21/contourpy-1.3.2-cp313-cp313t-win32.whl", hash = "sha256:d0e589ae0d55204991450bb5c23f571c64fe43adaa53f93fc902a84c96f52fe1", size = 189550 }, + { url = "https://files.pythonhosted.org/packages/b0/e6/6000d0094e8a5e32ad62591c8609e269febb6e4db83a1c75ff8868b42731/contourpy-1.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:78e9253c3de756b3f6a5174d024c4835acd59eb3f8e2ca13e775dbffe1558f69", size = 238214 }, + { url = "https://files.pythonhosted.org/packages/33/05/b26e3c6ecc05f349ee0013f0bb850a761016d89cec528a98193a48c34033/contourpy-1.3.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:fd93cc7f3139b6dd7aab2f26a90dde0aa9fc264dbf70f6740d498a70b860b82c", size = 265681 }, + { url = "https://files.pythonhosted.org/packages/2b/25/ac07d6ad12affa7d1ffed11b77417d0a6308170f44ff20fa1d5aa6333f03/contourpy-1.3.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:107ba8a6a7eec58bb475329e6d3b95deba9440667c4d62b9b6063942b61d7f16", size = 315101 }, + { url = "https://files.pythonhosted.org/packages/8f/4d/5bb3192bbe9d3f27e3061a6a8e7733c9120e203cb8515767d30973f71030/contourpy-1.3.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ded1706ed0c1049224531b81128efbd5084598f18d8a2d9efae833edbd2b40ad", size = 220599 }, + { url = "https://files.pythonhosted.org/packages/ff/c0/91f1215d0d9f9f343e4773ba6c9b89e8c0cc7a64a6263f21139da639d848/contourpy-1.3.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5f5964cdad279256c084b69c3f412b7801e15356b16efa9d78aa974041903da0", size = 266807 }, + { url = "https://files.pythonhosted.org/packages/d4/79/6be7e90c955c0487e7712660d6cead01fa17bff98e0ea275737cc2bc8e71/contourpy-1.3.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49b65a95d642d4efa8f64ba12558fcb83407e58a2dfba9d796d77b63ccfcaff5", size = 318729 }, + { url = "https://files.pythonhosted.org/packages/87/68/7f46fb537958e87427d98a4074bcde4b67a70b04900cfc5ce29bc2f556c1/contourpy-1.3.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8c5acb8dddb0752bf252e01a3035b21443158910ac16a3b0d20e7fed7d534ce5", size = 221791 }, +] + +[[package]] +name = "cycler" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321 }, +] + +[[package]] +name = "dill" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/12/80/630b4b88364e9a8c8c5797f4602d0f76ef820909ee32f0bacb9f90654042/dill-0.4.0.tar.gz", hash = "sha256:0633f1d2df477324f53a895b02c901fb961bdbf65a17122586ea7019292cbcf0", size = 186976 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/3d/9373ad9c56321fdab5b41197068e1d8c25883b3fea29dd361f9b55116869/dill-0.4.0-py3-none-any.whl", hash = "sha256:44f54bf6412c2c8464c14e8243eb163690a9800dbe2c367330883b19c7561049", size = 119668 }, +] + +[[package]] +name = "fonttools" +version = "4.59.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/27/ec3c723bfdf86f34c5c82bf6305df3e0f0d8ea798d2d3a7cb0c0a866d286/fonttools-4.59.0.tar.gz", hash = "sha256:be392ec3529e2f57faa28709d60723a763904f71a2b63aabe14fee6648fe3b14", size = 3532521 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1c/1f/3dcae710b7c4b56e79442b03db64f6c9f10c3348f7af40339dffcefb581e/fonttools-4.59.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:524133c1be38445c5c0575eacea42dbd44374b310b1ffc4b60ff01d881fabb96", size = 2761846 }, + { url = "https://files.pythonhosted.org/packages/eb/0e/ae3a1884fa1549acac1191cc9ec039142f6ac0e9cbc139c2e6a3dab967da/fonttools-4.59.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:21e606b2d38fed938dde871c5736822dd6bda7a4631b92e509a1f5cd1b90c5df", size = 2332060 }, + { url = "https://files.pythonhosted.org/packages/75/46/58bff92a7216829159ac7bdb1d05a48ad1b8ab8c539555f12d29fdecfdd4/fonttools-4.59.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e93df708c69a193fc7987192f94df250f83f3851fda49413f02ba5dded639482", size = 4852354 }, + { url = "https://files.pythonhosted.org/packages/05/57/767e31e48861045d89691128bd81fd4c62b62150f9a17a666f731ce4f197/fonttools-4.59.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:62224a9bb85b4b66d1b46d45cbe43d71cbf8f527d332b177e3b96191ffbc1e64", size = 4781132 }, + { url = "https://files.pythonhosted.org/packages/d7/78/adb5e9b0af5c6ce469e8b0e112f144eaa84b30dd72a486e9c778a9b03b31/fonttools-4.59.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b8974b2a266b54c96709bd5e239979cddfd2dbceed331aa567ea1d7c4a2202db", size = 4832901 }, + { url = "https://files.pythonhosted.org/packages/ac/92/bc3881097fbf3d56d112bec308c863c058e5d4c9c65f534e8ae58450ab8a/fonttools-4.59.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:209b75943d158f610b78320eacb5539aa9e920bee2c775445b2846c65d20e19d", size = 4940140 }, + { url = "https://files.pythonhosted.org/packages/4a/54/39cdb23f0eeda2e07ae9cb189f2b6f41da89aabc682d3a387b3ff4a4ed29/fonttools-4.59.0-cp310-cp310-win32.whl", hash = "sha256:4c908a7036f0f3677f8afa577bcd973e3e20ddd2f7c42a33208d18bee95cdb6f", size = 2215890 }, + { url = "https://files.pythonhosted.org/packages/d8/eb/f8388d9e19f95d8df2449febe9b1a38ddd758cfdb7d6de3a05198d785d61/fonttools-4.59.0-cp310-cp310-win_amd64.whl", hash = "sha256:8b4309a2775e4feee7356e63b163969a215d663399cce1b3d3b65e7ec2d9680e", size = 2260191 }, + { url = "https://files.pythonhosted.org/packages/06/96/520733d9602fa1bf6592e5354c6721ac6fc9ea72bc98d112d0c38b967199/fonttools-4.59.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:841b2186adce48903c0fef235421ae21549020eca942c1da773ac380b056ab3c", size = 2782387 }, + { url = "https://files.pythonhosted.org/packages/87/6a/170fce30b9bce69077d8eec9bea2cfd9f7995e8911c71be905e2eba6368b/fonttools-4.59.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9bcc1e77fbd1609198966ded6b2a9897bd6c6bcbd2287a2fc7d75f1a254179c5", size = 2342194 }, + { url = "https://files.pythonhosted.org/packages/b0/b6/7c8166c0066856f1408092f7968ac744060cf72ca53aec9036106f57eeca/fonttools-4.59.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:37c377f7cb2ab2eca8a0b319c68146d34a339792f9420fca6cd49cf28d370705", size = 5032333 }, + { url = "https://files.pythonhosted.org/packages/eb/0c/707c5a19598eafcafd489b73c4cb1c142102d6197e872f531512d084aa76/fonttools-4.59.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fa39475eaccb98f9199eccfda4298abaf35ae0caec676ffc25b3a5e224044464", size = 4974422 }, + { url = "https://files.pythonhosted.org/packages/f6/e7/6d33737d9fe632a0f59289b6f9743a86d2a9d0673de2a0c38c0f54729822/fonttools-4.59.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d3972b13148c1d1fbc092b27678a33b3080d1ac0ca305742b0119b75f9e87e38", size = 5010631 }, + { url = "https://files.pythonhosted.org/packages/63/e1/a4c3d089ab034a578820c8f2dff21ef60daf9668034a1e4fb38bb1cc3398/fonttools-4.59.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a408c3c51358c89b29cfa5317cf11518b7ce5de1717abb55c5ae2d2921027de6", size = 5122198 }, + { url = "https://files.pythonhosted.org/packages/09/77/ca82b9c12fa4de3c520b7760ee61787640cf3fde55ef1b0bfe1de38c8153/fonttools-4.59.0-cp311-cp311-win32.whl", hash = "sha256:6770d7da00f358183d8fd5c4615436189e4f683bdb6affb02cad3d221d7bb757", size = 2214216 }, + { url = "https://files.pythonhosted.org/packages/ab/25/5aa7ca24b560b2f00f260acf32c4cf29d7aaf8656e159a336111c18bc345/fonttools-4.59.0-cp311-cp311-win_amd64.whl", hash = "sha256:84fc186980231a287b28560d3123bd255d3c6b6659828c642b4cf961e2b923d0", size = 2261879 }, + { url = "https://files.pythonhosted.org/packages/e2/77/b1c8af22f4265e951cd2e5535dbef8859efcef4fb8dee742d368c967cddb/fonttools-4.59.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f9b3a78f69dcbd803cf2fb3f972779875b244c1115481dfbdd567b2c22b31f6b", size = 2767562 }, + { url = "https://files.pythonhosted.org/packages/ff/5a/aeb975699588176bb357e8b398dfd27e5d3a2230d92b81ab8cbb6187358d/fonttools-4.59.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:57bb7e26928573ee7c6504f54c05860d867fd35e675769f3ce01b52af38d48e2", size = 2335168 }, + { url = "https://files.pythonhosted.org/packages/54/97/c6101a7e60ae138c4ef75b22434373a0da50a707dad523dd19a4889315bf/fonttools-4.59.0-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4536f2695fe5c1ffb528d84a35a7d3967e5558d2af58b4775e7ab1449d65767b", size = 4909850 }, + { url = "https://files.pythonhosted.org/packages/bd/6c/fa4d18d641054f7bff878cbea14aa9433f292b9057cb1700d8e91a4d5f4f/fonttools-4.59.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:885bde7d26e5b40e15c47bd5def48b38cbd50830a65f98122a8fb90962af7cd1", size = 4955131 }, + { url = "https://files.pythonhosted.org/packages/20/5c/331947fc1377deb928a69bde49f9003364f5115e5cbe351eea99e39412a2/fonttools-4.59.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6801aeddb6acb2c42eafa45bc1cb98ba236871ae6f33f31e984670b749a8e58e", size = 4899667 }, + { url = "https://files.pythonhosted.org/packages/8a/46/b66469dfa26b8ff0baa7654b2cc7851206c6d57fe3abdabbaab22079a119/fonttools-4.59.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:31003b6a10f70742a63126b80863ab48175fb8272a18ca0846c0482968f0588e", size = 5051349 }, + { url = "https://files.pythonhosted.org/packages/2e/05/ebfb6b1f3a4328ab69787d106a7d92ccde77ce66e98659df0f9e3f28d93d/fonttools-4.59.0-cp312-cp312-win32.whl", hash = "sha256:fbce6dae41b692a5973d0f2158f782b9ad05babc2c2019a970a1094a23909b1b", size = 2201315 }, + { url = "https://files.pythonhosted.org/packages/09/45/d2bdc9ea20bbadec1016fd0db45696d573d7a26d95ab5174ffcb6d74340b/fonttools-4.59.0-cp312-cp312-win_amd64.whl", hash = "sha256:332bfe685d1ac58ca8d62b8d6c71c2e52a6c64bc218dc8f7825c9ea51385aa01", size = 2249408 }, + { url = "https://files.pythonhosted.org/packages/f3/bb/390990e7c457d377b00890d9f96a3ca13ae2517efafb6609c1756e213ba4/fonttools-4.59.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:78813b49d749e1bb4db1c57f2d4d7e6db22c253cb0a86ad819f5dc197710d4b2", size = 2758704 }, + { url = "https://files.pythonhosted.org/packages/df/6f/d730d9fcc9b410a11597092bd2eb9ca53e5438c6cb90e4b3047ce1b723e9/fonttools-4.59.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:401b1941ce37e78b8fd119b419b617277c65ae9417742a63282257434fd68ea2", size = 2330764 }, + { url = "https://files.pythonhosted.org/packages/75/b4/b96bb66f6f8cc4669de44a158099b249c8159231d254ab6b092909388be5/fonttools-4.59.0-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:efd7e6660674e234e29937bc1481dceb7e0336bfae75b856b4fb272b5093c5d4", size = 4890699 }, + { url = "https://files.pythonhosted.org/packages/b5/57/7969af50b26408be12baa317c6147588db5b38af2759e6df94554dbc5fdb/fonttools-4.59.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51ab1ff33c19e336c02dee1e9fd1abd974a4ca3d8f7eef2a104d0816a241ce97", size = 4952934 }, + { url = "https://files.pythonhosted.org/packages/d6/e2/dd968053b6cf1f46c904f5bd409b22341477c017d8201619a265e50762d3/fonttools-4.59.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a9bf8adc9e1f3012edc8f09b08336272aec0c55bc677422273e21280db748f7c", size = 4892319 }, + { url = "https://files.pythonhosted.org/packages/6b/95/a59810d8eda09129f83467a4e58f84205dc6994ebaeb9815406363e07250/fonttools-4.59.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:37e01c6ec0c98599778c2e688350d624fa4770fbd6144551bd5e032f1199171c", size = 5034753 }, + { url = "https://files.pythonhosted.org/packages/a5/84/51a69ee89ff8d1fea0c6997e946657e25a3f08513de8435fe124929f3eef/fonttools-4.59.0-cp313-cp313-win32.whl", hash = "sha256:70d6b3ceaa9cc5a6ac52884f3b3d9544e8e231e95b23f138bdb78e6d4dc0eae3", size = 2199688 }, + { url = "https://files.pythonhosted.org/packages/a0/ee/f626cd372932d828508137a79b85167fdcf3adab2e3bed433f295c596c6a/fonttools-4.59.0-cp313-cp313-win_amd64.whl", hash = "sha256:26731739daa23b872643f0e4072d5939960237d540c35c14e6a06d47d71ca8fe", size = 2248560 }, + { url = "https://files.pythonhosted.org/packages/c5/68/635adfcd75d86a965f633ea704308a762ee7e80f000456da010eadd3b032/fonttools-4.59.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8d77f92438daeaddc05682f0f3dac90c5b9829bcac75b57e8ce09cb67786073c", size = 2768038 }, + { url = "https://files.pythonhosted.org/packages/d4/c7/41812171da0337a4d3e58da0fe9e13df55990a8e48d1babf1ece2f48a717/fonttools-4.59.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:60f6665579e909b618282f3c14fa0b80570fbf1ee0e67678b9a9d43aa5d67a37", size = 2335207 }, + { url = "https://files.pythonhosted.org/packages/c9/40/0b1c47982ccb8c5eec15ddae486ccdf34364c2683307e139f877c6a4710f/fonttools-4.59.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:169b99a2553a227f7b5fea8d9ecd673aa258617f466b2abc6091fe4512a0dcd0", size = 4832505 }, + { url = "https://files.pythonhosted.org/packages/ee/40/70cfe1b4a3f6218457e76ce0743e692cb82a4e5c8a9a1fe64576428488a2/fonttools-4.59.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:052444a5d0151878e87e3e512a1aa1a0ab35ee4c28afde0a778e23b0ace4a7de", size = 4762567 }, + { url = "https://files.pythonhosted.org/packages/d4/18/5231342b4e528eb8d2c048f4663cf7dc892dee51387f5a0383b8e9e49283/fonttools-4.59.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:d40dcf533ca481355aa7b682e9e079f766f35715defa4929aeb5597f9604272e", size = 4815520 }, + { url = "https://files.pythonhosted.org/packages/91/b3/7661184576e235f84ed6ff232d287c598fb517224c5dfad8ae67fdd158e5/fonttools-4.59.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b818db35879d2edf7f46c7e729c700a0bce03b61b9412f5a7118406687cb151d", size = 4924601 }, + { url = "https://files.pythonhosted.org/packages/cf/96/dfd52d0e603c2c03f1b6153a1989c810a2083f8ad282b8b80acf3fe736f8/fonttools-4.59.0-cp39-cp39-win32.whl", hash = "sha256:2e7cf8044ce2598bb87e44ba1d2c6e45d7a8decf56055b92906dc53f67c76d64", size = 1485238 }, + { url = "https://files.pythonhosted.org/packages/3b/75/efc6486371cc1125e41fc0c149d80605e17ce7fc28c05cc33d503b0bf41f/fonttools-4.59.0-cp39-cp39-win_amd64.whl", hash = "sha256:902425f5afe28572d65d2bf9c33edd5265c612ff82c69e6f83ea13eafc0dcbea", size = 1530070 }, + { url = "https://files.pythonhosted.org/packages/d0/9c/df0ef2c51845a13043e5088f7bb988ca6cd5bb82d5d4203d6a158aa58cf2/fonttools-4.59.0-py3-none-any.whl", hash = "sha256:241313683afd3baacb32a6bd124d0bce7404bc5280e12e291bae1b9bba28711d", size = 1128050 }, +] + +[[package]] +name = "greyjack" +version = "0.3.2" +source = { editable = "." } +dependencies = [ + { name = "bitarray" }, + { name = "dill" }, + { name = "matplotlib", version = "3.9.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "matplotlib", version = "3.10.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "maturin" }, + { name = "mmh3" }, + { name = "multiprocess" }, + { name = "mypy" }, + { name = "mypy-extensions" }, + { name = "mypy-protobuf" }, + { name = "numba", version = "0.60.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "numba", version = "0.61.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pathos" }, + { name = "polars" }, + { name = "pyarrow" }, + { name = "pyzmq" }, +] + +[package.metadata] +requires-dist = [ + { name = "bitarray", specifier = ">=3.5.0" }, + { name = "dill" }, + { name = "matplotlib", specifier = ">=3.9.4" }, + { name = "maturin", specifier = ">=1.9.1" }, + { name = "mmh3", specifier = ">=5.1.0" }, + { name = "multiprocess" }, + { name = "mypy" }, + { name = "mypy-extensions" }, + { name = "mypy-protobuf" }, + { name = "numba", specifier = ">=0.60.0" }, + { name = "numpy" }, + { name = "pathos" }, + { name = "polars" }, + { name = "pyarrow" }, + { name = "pyzmq" }, +] + +[[package]] +name = "importlib-resources" +version = "6.5.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cf/8c/f834fbf984f691b4f7ff60f50b514cc3de5cc08abfc3295564dd89c5e2e7/importlib_resources-6.5.2.tar.gz", hash = "sha256:185f87adef5bcc288449d98fb4fba07cea78bc036455dd44c5fc4a2fe78fed2c", size = 44693 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/ed/1f1afb2e9e7f38a545d628f864d562a5ae64fe6f7a10e28ffb9b185b4e89/importlib_resources-6.5.2-py3-none-any.whl", hash = "sha256:789cfdc3ed28c78b67a06acb8126751ced69a3d5f79c095a98298cd8a760ccec", size = 37461 }, +] + +[[package]] +name = "kiwisolver" +version = "1.4.7" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/85/4d/2255e1c76304cbd60b48cee302b66d1dde4468dc5b1160e4b7cb43778f2a/kiwisolver-1.4.7.tar.gz", hash = "sha256:9893ff81bd7107f7b685d3017cc6583daadb4fc26e4a888350df530e41980a60", size = 97286 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/14/fc943dd65268a96347472b4fbe5dcc2f6f55034516f80576cd0dd3a8930f/kiwisolver-1.4.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8a9c83f75223d5e48b0bc9cb1bf2776cf01563e00ade8775ffe13b0b6e1af3a6", size = 122440 }, + { url = "https://files.pythonhosted.org/packages/1e/46/e68fed66236b69dd02fcdb506218c05ac0e39745d696d22709498896875d/kiwisolver-1.4.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:58370b1ffbd35407444d57057b57da5d6549d2d854fa30249771775c63b5fe17", size = 65758 }, + { url = "https://files.pythonhosted.org/packages/ef/fa/65de49c85838681fc9cb05de2a68067a683717321e01ddafb5b8024286f0/kiwisolver-1.4.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:aa0abdf853e09aff551db11fce173e2177d00786c688203f52c87ad7fcd91ef9", size = 64311 }, + { url = "https://files.pythonhosted.org/packages/42/9c/cc8d90f6ef550f65443bad5872ffa68f3dee36de4974768628bea7c14979/kiwisolver-1.4.7-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:8d53103597a252fb3ab8b5845af04c7a26d5e7ea8122303dd7a021176a87e8b9", size = 1637109 }, + { url = "https://files.pythonhosted.org/packages/55/91/0a57ce324caf2ff5403edab71c508dd8f648094b18cfbb4c8cc0fde4a6ac/kiwisolver-1.4.7-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:88f17c5ffa8e9462fb79f62746428dd57b46eb931698e42e990ad63103f35e6c", size = 1617814 }, + { url = "https://files.pythonhosted.org/packages/12/5d/c36140313f2510e20207708adf36ae4919416d697ee0236b0ddfb6fd1050/kiwisolver-1.4.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88a9ca9c710d598fd75ee5de59d5bda2684d9db36a9f50b6125eaea3969c2599", size = 1400881 }, + { url = "https://files.pythonhosted.org/packages/56/d0/786e524f9ed648324a466ca8df86298780ef2b29c25313d9a4f16992d3cf/kiwisolver-1.4.7-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f4d742cb7af1c28303a51b7a27aaee540e71bb8e24f68c736f6f2ffc82f2bf05", size = 1512972 }, + { url = "https://files.pythonhosted.org/packages/67/5a/77851f2f201e6141d63c10a0708e996a1363efaf9e1609ad0441b343763b/kiwisolver-1.4.7-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e28c7fea2196bf4c2f8d46a0415c77a1c480cc0724722f23d7410ffe9842c407", size = 1444787 }, + { url = "https://files.pythonhosted.org/packages/06/5f/1f5eaab84355885e224a6fc8d73089e8713dc7e91c121f00b9a1c58a2195/kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e968b84db54f9d42046cf154e02911e39c0435c9801681e3fc9ce8a3c4130278", size = 2199212 }, + { url = "https://files.pythonhosted.org/packages/b5/28/9152a3bfe976a0ae21d445415defc9d1cd8614b2910b7614b30b27a47270/kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0c18ec74c0472de033e1bebb2911c3c310eef5649133dd0bedf2a169a1b269e5", size = 2346399 }, + { url = "https://files.pythonhosted.org/packages/26/f6/453d1904c52ac3b400f4d5e240ac5fec25263716723e44be65f4d7149d13/kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8f0ea6da6d393d8b2e187e6a5e3fb81f5862010a40c3945e2c6d12ae45cfb2ad", size = 2308688 }, + { url = "https://files.pythonhosted.org/packages/5a/9a/d4968499441b9ae187e81745e3277a8b4d7c60840a52dc9d535a7909fac3/kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:f106407dda69ae456dd1227966bf445b157ccc80ba0dff3802bb63f30b74e895", size = 2445493 }, + { url = "https://files.pythonhosted.org/packages/07/c9/032267192e7828520dacb64dfdb1d74f292765f179e467c1cba97687f17d/kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:84ec80df401cfee1457063732d90022f93951944b5b58975d34ab56bb150dfb3", size = 2262191 }, + { url = "https://files.pythonhosted.org/packages/6c/ad/db0aedb638a58b2951da46ddaeecf204be8b4f5454df020d850c7fa8dca8/kiwisolver-1.4.7-cp310-cp310-win32.whl", hash = "sha256:71bb308552200fb2c195e35ef05de12f0c878c07fc91c270eb3d6e41698c3bcc", size = 46644 }, + { url = "https://files.pythonhosted.org/packages/12/ca/d0f7b7ffbb0be1e7c2258b53554efec1fd652921f10d7d85045aff93ab61/kiwisolver-1.4.7-cp310-cp310-win_amd64.whl", hash = "sha256:44756f9fd339de0fb6ee4f8c1696cfd19b2422e0d70b4cefc1cc7f1f64045a8c", size = 55877 }, + { url = "https://files.pythonhosted.org/packages/97/6c/cfcc128672f47a3e3c0d918ecb67830600078b025bfc32d858f2e2d5c6a4/kiwisolver-1.4.7-cp310-cp310-win_arm64.whl", hash = "sha256:78a42513018c41c2ffd262eb676442315cbfe3c44eed82385c2ed043bc63210a", size = 48347 }, + { url = "https://files.pythonhosted.org/packages/e9/44/77429fa0a58f941d6e1c58da9efe08597d2e86bf2b2cce6626834f49d07b/kiwisolver-1.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d2b0e12a42fb4e72d509fc994713d099cbb15ebf1103545e8a45f14da2dfca54", size = 122442 }, + { url = "https://files.pythonhosted.org/packages/e5/20/8c75caed8f2462d63c7fd65e16c832b8f76cda331ac9e615e914ee80bac9/kiwisolver-1.4.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2a8781ac3edc42ea4b90bc23e7d37b665d89423818e26eb6df90698aa2287c95", size = 65762 }, + { url = "https://files.pythonhosted.org/packages/f4/98/fe010f15dc7230f45bc4cf367b012d651367fd203caaa992fd1f5963560e/kiwisolver-1.4.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:46707a10836894b559e04b0fd143e343945c97fd170d69a2d26d640b4e297935", size = 64319 }, + { url = "https://files.pythonhosted.org/packages/8b/1b/b5d618f4e58c0675654c1e5051bcf42c776703edb21c02b8c74135541f60/kiwisolver-1.4.7-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef97b8df011141c9b0f6caf23b29379f87dd13183c978a30a3c546d2c47314cb", size = 1334260 }, + { url = "https://files.pythonhosted.org/packages/b8/01/946852b13057a162a8c32c4c8d2e9ed79f0bb5d86569a40c0b5fb103e373/kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ab58c12a2cd0fc769089e6d38466c46d7f76aced0a1f54c77652446733d2d02", size = 1426589 }, + { url = "https://files.pythonhosted.org/packages/70/d1/c9f96df26b459e15cf8a965304e6e6f4eb291e0f7a9460b4ad97b047561e/kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:803b8e1459341c1bb56d1c5c010406d5edec8a0713a0945851290a7930679b51", size = 1541080 }, + { url = "https://files.pythonhosted.org/packages/d3/73/2686990eb8b02d05f3de759d6a23a4ee7d491e659007dd4c075fede4b5d0/kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f9a9e8a507420fe35992ee9ecb302dab68550dedc0da9e2880dd88071c5fb052", size = 1470049 }, + { url = "https://files.pythonhosted.org/packages/a7/4b/2db7af3ed3af7c35f388d5f53c28e155cd402a55432d800c543dc6deb731/kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18077b53dc3bb490e330669a99920c5e6a496889ae8c63b58fbc57c3d7f33a18", size = 1426376 }, + { url = "https://files.pythonhosted.org/packages/05/83/2857317d04ea46dc5d115f0df7e676997bbd968ced8e2bd6f7f19cfc8d7f/kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6af936f79086a89b3680a280c47ea90b4df7047b5bdf3aa5c524bbedddb9e545", size = 2222231 }, + { url = "https://files.pythonhosted.org/packages/0d/b5/866f86f5897cd4ab6d25d22e403404766a123f138bd6a02ecb2cdde52c18/kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:3abc5b19d24af4b77d1598a585b8a719beb8569a71568b66f4ebe1fb0449460b", size = 2368634 }, + { url = "https://files.pythonhosted.org/packages/c1/ee/73de8385403faba55f782a41260210528fe3273d0cddcf6d51648202d6d0/kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:933d4de052939d90afbe6e9d5273ae05fb836cc86c15b686edd4b3560cc0ee36", size = 2329024 }, + { url = "https://files.pythonhosted.org/packages/a1/e7/cd101d8cd2cdfaa42dc06c433df17c8303d31129c9fdd16c0ea37672af91/kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:65e720d2ab2b53f1f72fb5da5fb477455905ce2c88aaa671ff0a447c2c80e8e3", size = 2468484 }, + { url = "https://files.pythonhosted.org/packages/e1/72/84f09d45a10bc57a40bb58b81b99d8f22b58b2040c912b7eb97ebf625bf2/kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3bf1ed55088f214ba6427484c59553123fdd9b218a42bbc8c6496d6754b1e523", size = 2284078 }, + { url = "https://files.pythonhosted.org/packages/d2/d4/71828f32b956612dc36efd7be1788980cb1e66bfb3706e6dec9acad9b4f9/kiwisolver-1.4.7-cp311-cp311-win32.whl", hash = "sha256:4c00336b9dd5ad96d0a558fd18a8b6f711b7449acce4c157e7343ba92dd0cf3d", size = 46645 }, + { url = "https://files.pythonhosted.org/packages/a1/65/d43e9a20aabcf2e798ad1aff6c143ae3a42cf506754bcb6a7ed8259c8425/kiwisolver-1.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:929e294c1ac1e9f615c62a4e4313ca1823ba37326c164ec720a803287c4c499b", size = 56022 }, + { url = "https://files.pythonhosted.org/packages/35/b3/9f75a2e06f1b4ca00b2b192bc2b739334127d27f1d0625627ff8479302ba/kiwisolver-1.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:e33e8fbd440c917106b237ef1a2f1449dfbb9b6f6e1ce17c94cd6a1e0d438376", size = 48536 }, + { url = "https://files.pythonhosted.org/packages/97/9c/0a11c714cf8b6ef91001c8212c4ef207f772dd84540104952c45c1f0a249/kiwisolver-1.4.7-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:5360cc32706dab3931f738d3079652d20982511f7c0ac5711483e6eab08efff2", size = 121808 }, + { url = "https://files.pythonhosted.org/packages/f2/d8/0fe8c5f5d35878ddd135f44f2af0e4e1d379e1c7b0716f97cdcb88d4fd27/kiwisolver-1.4.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:942216596dc64ddb25adb215c3c783215b23626f8d84e8eff8d6d45c3f29f75a", size = 65531 }, + { url = "https://files.pythonhosted.org/packages/80/c5/57fa58276dfdfa612241d640a64ca2f76adc6ffcebdbd135b4ef60095098/kiwisolver-1.4.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:48b571ecd8bae15702e4f22d3ff6a0f13e54d3d00cd25216d5e7f658242065ee", size = 63894 }, + { url = "https://files.pythonhosted.org/packages/8b/e9/26d3edd4c4ad1c5b891d8747a4f81b1b0aba9fb9721de6600a4adc09773b/kiwisolver-1.4.7-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ad42ba922c67c5f219097b28fae965e10045ddf145d2928bfac2eb2e17673640", size = 1369296 }, + { url = "https://files.pythonhosted.org/packages/b6/67/3f4850b5e6cffb75ec40577ddf54f7b82b15269cc5097ff2e968ee32ea7d/kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:612a10bdae23404a72941a0fc8fa2660c6ea1217c4ce0dbcab8a8f6543ea9e7f", size = 1461450 }, + { url = "https://files.pythonhosted.org/packages/52/be/86cbb9c9a315e98a8dc6b1d23c43cffd91d97d49318854f9c37b0e41cd68/kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9e838bba3a3bac0fe06d849d29772eb1afb9745a59710762e4ba3f4cb8424483", size = 1579168 }, + { url = "https://files.pythonhosted.org/packages/0f/00/65061acf64bd5fd34c1f4ae53f20b43b0a017a541f242a60b135b9d1e301/kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:22f499f6157236c19f4bbbd472fa55b063db77a16cd74d49afe28992dff8c258", size = 1507308 }, + { url = "https://files.pythonhosted.org/packages/21/e4/c0b6746fd2eb62fe702118b3ca0cb384ce95e1261cfada58ff693aeec08a/kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:693902d433cf585133699972b6d7c42a8b9f8f826ebcaf0132ff55200afc599e", size = 1464186 }, + { url = "https://files.pythonhosted.org/packages/0a/0f/529d0a9fffb4d514f2782c829b0b4b371f7f441d61aa55f1de1c614c4ef3/kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4e77f2126c3e0b0d055f44513ed349038ac180371ed9b52fe96a32aa071a5107", size = 2247877 }, + { url = "https://files.pythonhosted.org/packages/d1/e1/66603ad779258843036d45adcbe1af0d1a889a07af4635f8b4ec7dccda35/kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:657a05857bda581c3656bfc3b20e353c232e9193eb167766ad2dc58b56504948", size = 2404204 }, + { url = "https://files.pythonhosted.org/packages/8d/61/de5fb1ca7ad1f9ab7970e340a5b833d735df24689047de6ae71ab9d8d0e7/kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4bfa75a048c056a411f9705856abfc872558e33c055d80af6a380e3658766038", size = 2352461 }, + { url = "https://files.pythonhosted.org/packages/ba/d2/0edc00a852e369827f7e05fd008275f550353f1f9bcd55db9363d779fc63/kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:34ea1de54beef1c104422d210c47c7d2a4999bdecf42c7b5718fbe59a4cac383", size = 2501358 }, + { url = "https://files.pythonhosted.org/packages/84/15/adc15a483506aec6986c01fb7f237c3aec4d9ed4ac10b756e98a76835933/kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:90da3b5f694b85231cf93586dad5e90e2d71b9428f9aad96952c99055582f520", size = 2314119 }, + { url = "https://files.pythonhosted.org/packages/36/08/3a5bb2c53c89660863a5aa1ee236912269f2af8762af04a2e11df851d7b2/kiwisolver-1.4.7-cp312-cp312-win32.whl", hash = "sha256:18e0cca3e008e17fe9b164b55735a325140a5a35faad8de92dd80265cd5eb80b", size = 46367 }, + { url = "https://files.pythonhosted.org/packages/19/93/c05f0a6d825c643779fc3c70876bff1ac221f0e31e6f701f0e9578690d70/kiwisolver-1.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:58cb20602b18f86f83a5c87d3ee1c766a79c0d452f8def86d925e6c60fbf7bfb", size = 55884 }, + { url = "https://files.pythonhosted.org/packages/d2/f9/3828d8f21b6de4279f0667fb50a9f5215e6fe57d5ec0d61905914f5b6099/kiwisolver-1.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:f5a8b53bdc0b3961f8b6125e198617c40aeed638b387913bf1ce78afb1b0be2a", size = 48528 }, + { url = "https://files.pythonhosted.org/packages/c4/06/7da99b04259b0f18b557a4effd1b9c901a747f7fdd84cf834ccf520cb0b2/kiwisolver-1.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2e6039dcbe79a8e0f044f1c39db1986a1b8071051efba3ee4d74f5b365f5226e", size = 121913 }, + { url = "https://files.pythonhosted.org/packages/97/f5/b8a370d1aa593c17882af0a6f6755aaecd643640c0ed72dcfd2eafc388b9/kiwisolver-1.4.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a1ecf0ac1c518487d9d23b1cd7139a6a65bc460cd101ab01f1be82ecf09794b6", size = 65627 }, + { url = "https://files.pythonhosted.org/packages/2a/fc/6c0374f7503522539e2d4d1b497f5ebad3f8ed07ab51aed2af988dd0fb65/kiwisolver-1.4.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7ab9ccab2b5bd5702ab0803676a580fffa2aa178c2badc5557a84cc943fcf750", size = 63888 }, + { url = "https://files.pythonhosted.org/packages/bf/3e/0b7172793d0f41cae5c923492da89a2ffcd1adf764c16159ca047463ebd3/kiwisolver-1.4.7-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f816dd2277f8d63d79f9c8473a79fe54047bc0467754962840782c575522224d", size = 1369145 }, + { url = "https://files.pythonhosted.org/packages/77/92/47d050d6f6aced2d634258123f2688fbfef8ded3c5baf2c79d94d91f1f58/kiwisolver-1.4.7-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf8bcc23ceb5a1b624572a1623b9f79d2c3b337c8c455405ef231933a10da379", size = 1461448 }, + { url = "https://files.pythonhosted.org/packages/9c/1b/8f80b18e20b3b294546a1adb41701e79ae21915f4175f311a90d042301cf/kiwisolver-1.4.7-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dea0bf229319828467d7fca8c7c189780aa9ff679c94539eed7532ebe33ed37c", size = 1578750 }, + { url = "https://files.pythonhosted.org/packages/a4/fe/fe8e72f3be0a844f257cadd72689c0848c6d5c51bc1d60429e2d14ad776e/kiwisolver-1.4.7-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c06a4c7cf15ec739ce0e5971b26c93638730090add60e183530d70848ebdd34", size = 1507175 }, + { url = "https://files.pythonhosted.org/packages/39/fa/cdc0b6105d90eadc3bee525fecc9179e2b41e1ce0293caaf49cb631a6aaf/kiwisolver-1.4.7-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:913983ad2deb14e66d83c28b632fd35ba2b825031f2fa4ca29675e665dfecbe1", size = 1463963 }, + { url = "https://files.pythonhosted.org/packages/6e/5c/0c03c4e542720c6177d4f408e56d1c8315899db72d46261a4e15b8b33a41/kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5337ec7809bcd0f424c6b705ecf97941c46279cf5ed92311782c7c9c2026f07f", size = 2248220 }, + { url = "https://files.pythonhosted.org/packages/3d/ee/55ef86d5a574f4e767df7da3a3a7ff4954c996e12d4fbe9c408170cd7dcc/kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4c26ed10c4f6fa6ddb329a5120ba3b6db349ca192ae211e882970bfc9d91420b", size = 2404463 }, + { url = "https://files.pythonhosted.org/packages/0f/6d/73ad36170b4bff4825dc588acf4f3e6319cb97cd1fb3eb04d9faa6b6f212/kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c619b101e6de2222c1fcb0531e1b17bbffbe54294bfba43ea0d411d428618c27", size = 2352842 }, + { url = "https://files.pythonhosted.org/packages/0b/16/fa531ff9199d3b6473bb4d0f47416cdb08d556c03b8bc1cccf04e756b56d/kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:073a36c8273647592ea332e816e75ef8da5c303236ec0167196793eb1e34657a", size = 2501635 }, + { url = "https://files.pythonhosted.org/packages/78/7e/aa9422e78419db0cbe75fb86d8e72b433818f2e62e2e394992d23d23a583/kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3ce6b2b0231bda412463e152fc18335ba32faf4e8c23a754ad50ffa70e4091ee", size = 2314556 }, + { url = "https://files.pythonhosted.org/packages/a8/b2/15f7f556df0a6e5b3772a1e076a9d9f6c538ce5f05bd590eca8106508e06/kiwisolver-1.4.7-cp313-cp313-win32.whl", hash = "sha256:f4c9aee212bc89d4e13f58be11a56cc8036cabad119259d12ace14b34476fd07", size = 46364 }, + { url = "https://files.pythonhosted.org/packages/0b/db/32e897e43a330eee8e4770bfd2737a9584b23e33587a0812b8e20aac38f7/kiwisolver-1.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:8a3ec5aa8e38fc4c8af308917ce12c536f1c88452ce554027e55b22cbbfbff76", size = 55887 }, + { url = "https://files.pythonhosted.org/packages/c8/a4/df2bdca5270ca85fd25253049eb6708d4127be2ed0e5c2650217450b59e9/kiwisolver-1.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:76c8094ac20ec259471ac53e774623eb62e6e1f56cd8690c67ce6ce4fcb05650", size = 48530 }, + { url = "https://files.pythonhosted.org/packages/11/88/37ea0ea64512997b13d69772db8dcdc3bfca5442cda3a5e4bb943652ee3e/kiwisolver-1.4.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3f9362ecfca44c863569d3d3c033dbe8ba452ff8eed6f6b5806382741a1334bd", size = 122449 }, + { url = "https://files.pythonhosted.org/packages/4e/45/5a5c46078362cb3882dcacad687c503089263c017ca1241e0483857791eb/kiwisolver-1.4.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e8df2eb9b2bac43ef8b082e06f750350fbbaf2887534a5be97f6cf07b19d9583", size = 65757 }, + { url = "https://files.pythonhosted.org/packages/8a/be/a6ae58978772f685d48dd2e84460937761c53c4bbd84e42b0336473d9775/kiwisolver-1.4.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f32d6edbc638cde7652bd690c3e728b25332acbadd7cad670cc4a02558d9c417", size = 64312 }, + { url = "https://files.pythonhosted.org/packages/f4/04/18ef6f452d311e1e1eb180c9bf5589187fa1f042db877e6fe443ef10099c/kiwisolver-1.4.7-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e2e6c39bd7b9372b0be21456caab138e8e69cc0fc1190a9dfa92bd45a1e6e904", size = 1626966 }, + { url = "https://files.pythonhosted.org/packages/21/b1/40655f6c3fa11ce740e8a964fa8e4c0479c87d6a7944b95af799c7a55dfe/kiwisolver-1.4.7-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:dda56c24d869b1193fcc763f1284b9126550eaf84b88bbc7256e15028f19188a", size = 1607044 }, + { url = "https://files.pythonhosted.org/packages/fd/93/af67dbcfb9b3323bbd2c2db1385a7139d8f77630e4a37bb945b57188eb2d/kiwisolver-1.4.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79849239c39b5e1fd906556c474d9b0439ea6792b637511f3fe3a41158d89ca8", size = 1391879 }, + { url = "https://files.pythonhosted.org/packages/40/6f/d60770ef98e77b365d96061d090c0cd9e23418121c55fff188fa4bdf0b54/kiwisolver-1.4.7-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5e3bc157fed2a4c02ec468de4ecd12a6e22818d4f09cde2c31ee3226ffbefab2", size = 1504751 }, + { url = "https://files.pythonhosted.org/packages/fa/3a/5f38667d313e983c432f3fcd86932177519ed8790c724e07d77d1de0188a/kiwisolver-1.4.7-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3da53da805b71e41053dc670f9a820d1157aae77b6b944e08024d17bcd51ef88", size = 1436990 }, + { url = "https://files.pythonhosted.org/packages/cb/3b/1520301a47326e6a6043b502647e42892be33b3f051e9791cc8bb43f1a32/kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8705f17dfeb43139a692298cb6637ee2e59c0194538153e83e9ee0c75c2eddde", size = 2191122 }, + { url = "https://files.pythonhosted.org/packages/cf/c4/eb52da300c166239a2233f1f9c4a1b767dfab98fae27681bfb7ea4873cb6/kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:82a5c2f4b87c26bb1a0ef3d16b5c4753434633b83d365cc0ddf2770c93829e3c", size = 2338126 }, + { url = "https://files.pythonhosted.org/packages/1a/cb/42b92fd5eadd708dd9107c089e817945500685f3437ce1fd387efebc6d6e/kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ce8be0466f4c0d585cdb6c1e2ed07232221df101a4c6f28821d2aa754ca2d9e2", size = 2298313 }, + { url = "https://files.pythonhosted.org/packages/4f/eb/be25aa791fe5fc75a8b1e0c965e00f942496bc04635c9aae8035f6b76dcd/kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:409afdfe1e2e90e6ee7fc896f3df9a7fec8e793e58bfa0d052c8a82f99c37abb", size = 2437784 }, + { url = "https://files.pythonhosted.org/packages/c5/22/30a66be7f3368d76ff95689e1c2e28d382383952964ab15330a15d8bfd03/kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5b9c3f4ee0b9a439d2415012bd1b1cc2df59e4d6a9939f4d669241d30b414327", size = 2253988 }, + { url = "https://files.pythonhosted.org/packages/35/d3/5f2ecb94b5211c8a04f218a76133cc8d6d153b0f9cd0b45fad79907f0689/kiwisolver-1.4.7-cp39-cp39-win32.whl", hash = "sha256:a79ae34384df2b615eefca647a2873842ac3b596418032bef9a7283675962644", size = 46980 }, + { url = "https://files.pythonhosted.org/packages/ef/17/cd10d020578764ea91740204edc6b3236ed8106228a46f568d716b11feb2/kiwisolver-1.4.7-cp39-cp39-win_amd64.whl", hash = "sha256:cf0438b42121a66a3a667de17e779330fc0f20b0d97d59d2f2121e182b0505e4", size = 55847 }, + { url = "https://files.pythonhosted.org/packages/91/84/32232502020bd78d1d12be7afde15811c64a95ed1f606c10456db4e4c3ac/kiwisolver-1.4.7-cp39-cp39-win_arm64.whl", hash = "sha256:764202cc7e70f767dab49e8df52c7455e8de0df5d858fa801a11aa0d882ccf3f", size = 48494 }, + { url = "https://files.pythonhosted.org/packages/ac/59/741b79775d67ab67ced9bb38552da688c0305c16e7ee24bba7a2be253fb7/kiwisolver-1.4.7-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:94252291e3fe68001b1dd747b4c0b3be12582839b95ad4d1b641924d68fd4643", size = 59491 }, + { url = "https://files.pythonhosted.org/packages/58/cc/fb239294c29a5656e99e3527f7369b174dd9cc7c3ef2dea7cb3c54a8737b/kiwisolver-1.4.7-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5b7dfa3b546da08a9f622bb6becdb14b3e24aaa30adba66749d38f3cc7ea9706", size = 57648 }, + { url = "https://files.pythonhosted.org/packages/3b/ef/2f009ac1f7aab9f81efb2d837301d255279d618d27b6015780115ac64bdd/kiwisolver-1.4.7-pp310-pypy310_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd3de6481f4ed8b734da5df134cd5a6a64fe32124fe83dde1e5b5f29fe30b1e6", size = 84257 }, + { url = "https://files.pythonhosted.org/packages/81/e1/c64f50987f85b68b1c52b464bb5bf73e71570c0f7782d626d1eb283ad620/kiwisolver-1.4.7-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a91b5f9f1205845d488c928e8570dcb62b893372f63b8b6e98b863ebd2368ff2", size = 80906 }, + { url = "https://files.pythonhosted.org/packages/fd/71/1687c5c0a0be2cee39a5c9c389e546f9c6e215e46b691d00d9f646892083/kiwisolver-1.4.7-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40fa14dbd66b8b8f470d5fc79c089a66185619d31645f9b0773b88b19f7223c4", size = 79951 }, + { url = "https://files.pythonhosted.org/packages/ea/8b/d7497df4a1cae9367adf21665dd1f896c2a7aeb8769ad77b662c5e2bcce7/kiwisolver-1.4.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:eb542fe7933aa09d8d8f9d9097ef37532a7df6497819d16efe4359890a2f417a", size = 55715 }, + { url = "https://files.pythonhosted.org/packages/d5/df/ce37d9b26f07ab90880923c94d12a6ff4d27447096b4c849bfc4339ccfdf/kiwisolver-1.4.7-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:8b01aac285f91ca889c800042c35ad3b239e704b150cfd3382adfc9dcc780e39", size = 58666 }, + { url = "https://files.pythonhosted.org/packages/b0/d3/e4b04f43bc629ac8e186b77b2b1a251cdfa5b7610fa189dc0db622672ce6/kiwisolver-1.4.7-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:48be928f59a1f5c8207154f935334d374e79f2b5d212826307d072595ad76a2e", size = 57088 }, + { url = "https://files.pythonhosted.org/packages/30/1c/752df58e2d339e670a535514d2db4fe8c842ce459776b8080fbe08ebb98e/kiwisolver-1.4.7-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f37cfe618a117e50d8c240555331160d73d0411422b59b5ee217843d7b693608", size = 84321 }, + { url = "https://files.pythonhosted.org/packages/f0/f8/fe6484e847bc6e238ec9f9828089fb2c0bb53f2f5f3a79351fde5b565e4f/kiwisolver-1.4.7-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:599b5c873c63a1f6ed7eead644a8a380cfbdf5db91dcb6f85707aaab213b1674", size = 80776 }, + { url = "https://files.pythonhosted.org/packages/9b/57/d7163c0379f250ef763aba85330a19feefb5ce6cb541ade853aaba881524/kiwisolver-1.4.7-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:801fa7802e5cfabe3ab0c81a34c323a319b097dfb5004be950482d882f3d7225", size = 79984 }, + { url = "https://files.pythonhosted.org/packages/8c/95/4a103776c265d13b3d2cd24fb0494d4e04ea435a8ef97e1b2c026d43250b/kiwisolver-1.4.7-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:0c6c43471bc764fad4bc99c5c2d6d16a676b1abf844ca7c8702bdae92df01ee0", size = 55811 }, +] + +[[package]] +name = "kiwisolver" +version = "1.4.8" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/82/59/7c91426a8ac292e1cdd53a63b6d9439abd573c875c3f92c146767dd33faf/kiwisolver-1.4.8.tar.gz", hash = "sha256:23d5f023bdc8c7e54eb65f03ca5d5bb25b601eac4d7f1a042888a1f45237987e", size = 97538 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/5f/4d8e9e852d98ecd26cdf8eaf7ed8bc33174033bba5e07001b289f07308fd/kiwisolver-1.4.8-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88c6f252f6816a73b1f8c904f7bbe02fd67c09a69f7cb8a0eecdbf5ce78e63db", size = 124623 }, + { url = "https://files.pythonhosted.org/packages/1d/70/7f5af2a18a76fe92ea14675f8bd88ce53ee79e37900fa5f1a1d8e0b42998/kiwisolver-1.4.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c72941acb7b67138f35b879bbe85be0f6c6a70cab78fe3ef6db9c024d9223e5b", size = 66720 }, + { url = "https://files.pythonhosted.org/packages/c6/13/e15f804a142353aefd089fadc8f1d985561a15358c97aca27b0979cb0785/kiwisolver-1.4.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ce2cf1e5688edcb727fdf7cd1bbd0b6416758996826a8be1d958f91880d0809d", size = 65413 }, + { url = "https://files.pythonhosted.org/packages/ce/6d/67d36c4d2054e83fb875c6b59d0809d5c530de8148846b1370475eeeece9/kiwisolver-1.4.8-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c8bf637892dc6e6aad2bc6d4d69d08764166e5e3f69d469e55427b6ac001b19d", size = 1650826 }, + { url = "https://files.pythonhosted.org/packages/de/c6/7b9bb8044e150d4d1558423a1568e4f227193662a02231064e3824f37e0a/kiwisolver-1.4.8-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:034d2c891f76bd3edbdb3ea11140d8510dca675443da7304205a2eaa45d8334c", size = 1628231 }, + { url = "https://files.pythonhosted.org/packages/b6/38/ad10d437563063eaaedbe2c3540a71101fc7fb07a7e71f855e93ea4de605/kiwisolver-1.4.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d47b28d1dfe0793d5e96bce90835e17edf9a499b53969b03c6c47ea5985844c3", size = 1408938 }, + { url = "https://files.pythonhosted.org/packages/52/ce/c0106b3bd7f9e665c5f5bc1e07cc95b5dabd4e08e3dad42dbe2faad467e7/kiwisolver-1.4.8-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb158fe28ca0c29f2260cca8c43005329ad58452c36f0edf298204de32a9a3ed", size = 1422799 }, + { url = "https://files.pythonhosted.org/packages/d0/87/efb704b1d75dc9758087ba374c0f23d3254505edaedd09cf9d247f7878b9/kiwisolver-1.4.8-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5536185fce131780ebd809f8e623bf4030ce1b161353166c49a3c74c287897f", size = 1354362 }, + { url = "https://files.pythonhosted.org/packages/eb/b3/fd760dc214ec9a8f208b99e42e8f0130ff4b384eca8b29dd0efc62052176/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:369b75d40abedc1da2c1f4de13f3482cb99e3237b38726710f4a793432b1c5ff", size = 2222695 }, + { url = "https://files.pythonhosted.org/packages/a2/09/a27fb36cca3fc01700687cc45dae7a6a5f8eeb5f657b9f710f788748e10d/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:641f2ddf9358c80faa22e22eb4c9f54bd3f0e442e038728f500e3b978d00aa7d", size = 2370802 }, + { url = "https://files.pythonhosted.org/packages/3d/c3/ba0a0346db35fe4dc1f2f2cf8b99362fbb922d7562e5f911f7ce7a7b60fa/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d561d2d8883e0819445cfe58d7ddd673e4015c3c57261d7bdcd3710d0d14005c", size = 2334646 }, + { url = "https://files.pythonhosted.org/packages/41/52/942cf69e562f5ed253ac67d5c92a693745f0bed3c81f49fc0cbebe4d6b00/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:1732e065704b47c9afca7ffa272f845300a4eb959276bf6970dc07265e73b605", size = 2467260 }, + { url = "https://files.pythonhosted.org/packages/32/26/2d9668f30d8a494b0411d4d7d4ea1345ba12deb6a75274d58dd6ea01e951/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bcb1ebc3547619c3b58a39e2448af089ea2ef44b37988caf432447374941574e", size = 2288633 }, + { url = "https://files.pythonhosted.org/packages/98/99/0dd05071654aa44fe5d5e350729961e7bb535372935a45ac89a8924316e6/kiwisolver-1.4.8-cp310-cp310-win_amd64.whl", hash = "sha256:89c107041f7b27844179ea9c85d6da275aa55ecf28413e87624d033cf1f6b751", size = 71885 }, + { url = "https://files.pythonhosted.org/packages/6c/fc/822e532262a97442989335394d441cd1d0448c2e46d26d3e04efca84df22/kiwisolver-1.4.8-cp310-cp310-win_arm64.whl", hash = "sha256:b5773efa2be9eb9fcf5415ea3ab70fc785d598729fd6057bea38d539ead28271", size = 65175 }, + { url = "https://files.pythonhosted.org/packages/da/ed/c913ee28936c371418cb167b128066ffb20bbf37771eecc2c97edf8a6e4c/kiwisolver-1.4.8-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a4d3601908c560bdf880f07d94f31d734afd1bb71e96585cace0e38ef44c6d84", size = 124635 }, + { url = "https://files.pythonhosted.org/packages/4c/45/4a7f896f7467aaf5f56ef093d1f329346f3b594e77c6a3c327b2d415f521/kiwisolver-1.4.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:856b269c4d28a5c0d5e6c1955ec36ebfd1651ac00e1ce0afa3e28da95293b561", size = 66717 }, + { url = "https://files.pythonhosted.org/packages/5f/b4/c12b3ac0852a3a68f94598d4c8d569f55361beef6159dce4e7b624160da2/kiwisolver-1.4.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c2b9a96e0f326205af81a15718a9073328df1173a2619a68553decb7097fd5d7", size = 65413 }, + { url = "https://files.pythonhosted.org/packages/a9/98/1df4089b1ed23d83d410adfdc5947245c753bddfbe06541c4aae330e9e70/kiwisolver-1.4.8-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c5020c83e8553f770cb3b5fc13faac40f17e0b205bd237aebd21d53d733adb03", size = 1343994 }, + { url = "https://files.pythonhosted.org/packages/8d/bf/b4b169b050c8421a7c53ea1ea74e4ef9c335ee9013216c558a047f162d20/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dace81d28c787956bfbfbbfd72fdcef014f37d9b48830829e488fdb32b49d954", size = 1434804 }, + { url = "https://files.pythonhosted.org/packages/66/5a/e13bd341fbcf73325ea60fdc8af752addf75c5079867af2e04cc41f34434/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:11e1022b524bd48ae56c9b4f9296bce77e15a2e42a502cceba602f804b32bb79", size = 1450690 }, + { url = "https://files.pythonhosted.org/packages/9b/4f/5955dcb376ba4a830384cc6fab7d7547bd6759fe75a09564910e9e3bb8ea/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b9b4d2892fefc886f30301cdd80debd8bb01ecdf165a449eb6e78f79f0fabd6", size = 1376839 }, + { url = "https://files.pythonhosted.org/packages/3a/97/5edbed69a9d0caa2e4aa616ae7df8127e10f6586940aa683a496c2c280b9/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a96c0e790ee875d65e340ab383700e2b4891677b7fcd30a699146f9384a2bb0", size = 1435109 }, + { url = "https://files.pythonhosted.org/packages/13/fc/e756382cb64e556af6c1809a1bbb22c141bbc2445049f2da06b420fe52bf/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:23454ff084b07ac54ca8be535f4174170c1094a4cff78fbae4f73a4bcc0d4dab", size = 2245269 }, + { url = "https://files.pythonhosted.org/packages/76/15/e59e45829d7f41c776d138245cabae6515cb4eb44b418f6d4109c478b481/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:87b287251ad6488e95b4f0b4a79a6d04d3ea35fde6340eb38fbd1ca9cd35bbbc", size = 2393468 }, + { url = "https://files.pythonhosted.org/packages/e9/39/483558c2a913ab8384d6e4b66a932406f87c95a6080112433da5ed668559/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b21dbe165081142b1232a240fc6383fd32cdd877ca6cc89eab93e5f5883e1c25", size = 2355394 }, + { url = "https://files.pythonhosted.org/packages/01/aa/efad1fbca6570a161d29224f14b082960c7e08268a133fe5dc0f6906820e/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:768cade2c2df13db52475bd28d3a3fac8c9eff04b0e9e2fda0f3760f20b3f7fc", size = 2490901 }, + { url = "https://files.pythonhosted.org/packages/c9/4f/15988966ba46bcd5ab9d0c8296914436720dd67fca689ae1a75b4ec1c72f/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d47cfb2650f0e103d4bf68b0b5804c68da97272c84bb12850d877a95c056bd67", size = 2312306 }, + { url = "https://files.pythonhosted.org/packages/2d/27/bdf1c769c83f74d98cbc34483a972f221440703054894a37d174fba8aa68/kiwisolver-1.4.8-cp311-cp311-win_amd64.whl", hash = "sha256:ed33ca2002a779a2e20eeb06aea7721b6e47f2d4b8a8ece979d8ba9e2a167e34", size = 71966 }, + { url = "https://files.pythonhosted.org/packages/4a/c9/9642ea855604aeb2968a8e145fc662edf61db7632ad2e4fb92424be6b6c0/kiwisolver-1.4.8-cp311-cp311-win_arm64.whl", hash = "sha256:16523b40aab60426ffdebe33ac374457cf62863e330a90a0383639ce14bf44b2", size = 65311 }, + { url = "https://files.pythonhosted.org/packages/fc/aa/cea685c4ab647f349c3bc92d2daf7ae34c8e8cf405a6dcd3a497f58a2ac3/kiwisolver-1.4.8-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d6af5e8815fd02997cb6ad9bbed0ee1e60014438ee1a5c2444c96f87b8843502", size = 124152 }, + { url = "https://files.pythonhosted.org/packages/c5/0b/8db6d2e2452d60d5ebc4ce4b204feeb16176a851fd42462f66ade6808084/kiwisolver-1.4.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bade438f86e21d91e0cf5dd7c0ed00cda0f77c8c1616bd83f9fc157fa6760d31", size = 66555 }, + { url = "https://files.pythonhosted.org/packages/60/26/d6a0db6785dd35d3ba5bf2b2df0aedc5af089962c6eb2cbf67a15b81369e/kiwisolver-1.4.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b83dc6769ddbc57613280118fb4ce3cd08899cc3369f7d0e0fab518a7cf37fdb", size = 65067 }, + { url = "https://files.pythonhosted.org/packages/c9/ed/1d97f7e3561e09757a196231edccc1bcf59d55ddccefa2afc9c615abd8e0/kiwisolver-1.4.8-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:111793b232842991be367ed828076b03d96202c19221b5ebab421ce8bcad016f", size = 1378443 }, + { url = "https://files.pythonhosted.org/packages/29/61/39d30b99954e6b46f760e6289c12fede2ab96a254c443639052d1b573fbc/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:257af1622860e51b1a9d0ce387bf5c2c4f36a90594cb9514f55b074bcc787cfc", size = 1472728 }, + { url = "https://files.pythonhosted.org/packages/0c/3e/804163b932f7603ef256e4a715e5843a9600802bb23a68b4e08c8c0ff61d/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:69b5637c3f316cab1ec1c9a12b8c5f4750a4c4b71af9157645bf32830e39c03a", size = 1478388 }, + { url = "https://files.pythonhosted.org/packages/8a/9e/60eaa75169a154700be74f875a4d9961b11ba048bef315fbe89cb6999056/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:782bb86f245ec18009890e7cb8d13a5ef54dcf2ebe18ed65f795e635a96a1c6a", size = 1413849 }, + { url = "https://files.pythonhosted.org/packages/bc/b3/9458adb9472e61a998c8c4d95cfdfec91c73c53a375b30b1428310f923e4/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc978a80a0db3a66d25767b03688f1147a69e6237175c0f4ffffaaedf744055a", size = 1475533 }, + { url = "https://files.pythonhosted.org/packages/e4/7a/0a42d9571e35798de80aef4bb43a9b672aa7f8e58643d7bd1950398ffb0a/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:36dbbfd34838500a31f52c9786990d00150860e46cd5041386f217101350f0d3", size = 2268898 }, + { url = "https://files.pythonhosted.org/packages/d9/07/1255dc8d80271400126ed8db35a1795b1a2c098ac3a72645075d06fe5c5d/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:eaa973f1e05131de5ff3569bbba7f5fd07ea0595d3870ed4a526d486fe57fa1b", size = 2425605 }, + { url = "https://files.pythonhosted.org/packages/84/df/5a3b4cf13780ef6f6942df67b138b03b7e79e9f1f08f57c49957d5867f6e/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a66f60f8d0c87ab7f59b6fb80e642ebb29fec354a4dfad687ca4092ae69d04f4", size = 2375801 }, + { url = "https://files.pythonhosted.org/packages/8f/10/2348d068e8b0f635c8c86892788dac7a6b5c0cb12356620ab575775aad89/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:858416b7fb777a53f0c59ca08190ce24e9abbd3cffa18886a5781b8e3e26f65d", size = 2520077 }, + { url = "https://files.pythonhosted.org/packages/32/d8/014b89fee5d4dce157d814303b0fce4d31385a2af4c41fed194b173b81ac/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:085940635c62697391baafaaeabdf3dd7a6c3643577dde337f4d66eba021b2b8", size = 2338410 }, + { url = "https://files.pythonhosted.org/packages/bd/72/dfff0cc97f2a0776e1c9eb5bef1ddfd45f46246c6533b0191887a427bca5/kiwisolver-1.4.8-cp312-cp312-win_amd64.whl", hash = "sha256:01c3d31902c7db5fb6182832713d3b4122ad9317c2c5877d0539227d96bb2e50", size = 71853 }, + { url = "https://files.pythonhosted.org/packages/dc/85/220d13d914485c0948a00f0b9eb419efaf6da81b7d72e88ce2391f7aed8d/kiwisolver-1.4.8-cp312-cp312-win_arm64.whl", hash = "sha256:a3c44cb68861de93f0c4a8175fbaa691f0aa22550c331fefef02b618a9dcb476", size = 65424 }, + { url = "https://files.pythonhosted.org/packages/79/b3/e62464a652f4f8cd9006e13d07abad844a47df1e6537f73ddfbf1bc997ec/kiwisolver-1.4.8-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:1c8ceb754339793c24aee1c9fb2485b5b1f5bb1c2c214ff13368431e51fc9a09", size = 124156 }, + { url = "https://files.pythonhosted.org/packages/8d/2d/f13d06998b546a2ad4f48607a146e045bbe48030774de29f90bdc573df15/kiwisolver-1.4.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a62808ac74b5e55a04a408cda6156f986cefbcf0ada13572696b507cc92fa1", size = 66555 }, + { url = "https://files.pythonhosted.org/packages/59/e3/b8bd14b0a54998a9fd1e8da591c60998dc003618cb19a3f94cb233ec1511/kiwisolver-1.4.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:68269e60ee4929893aad82666821aaacbd455284124817af45c11e50a4b42e3c", size = 65071 }, + { url = "https://files.pythonhosted.org/packages/f0/1c/6c86f6d85ffe4d0ce04228d976f00674f1df5dc893bf2dd4f1928748f187/kiwisolver-1.4.8-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:34d142fba9c464bc3bbfeff15c96eab0e7310343d6aefb62a79d51421fcc5f1b", size = 1378053 }, + { url = "https://files.pythonhosted.org/packages/4e/b9/1c6e9f6dcb103ac5cf87cb695845f5fa71379021500153566d8a8a9fc291/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ddc373e0eef45b59197de815b1b28ef89ae3955e7722cc9710fb91cd77b7f47", size = 1472278 }, + { url = "https://files.pythonhosted.org/packages/ee/81/aca1eb176de671f8bda479b11acdc42c132b61a2ac861c883907dde6debb/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:77e6f57a20b9bd4e1e2cedda4d0b986ebd0216236f0106e55c28aea3d3d69b16", size = 1478139 }, + { url = "https://files.pythonhosted.org/packages/49/f4/e081522473671c97b2687d380e9e4c26f748a86363ce5af48b4a28e48d06/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08e77738ed7538f036cd1170cbed942ef749137b1311fa2bbe2a7fda2f6bf3cc", size = 1413517 }, + { url = "https://files.pythonhosted.org/packages/8f/e9/6a7d025d8da8c4931522922cd706105aa32b3291d1add8c5427cdcd66e63/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5ce1e481a74b44dd5e92ff03ea0cb371ae7a0268318e202be06c8f04f4f1246", size = 1474952 }, + { url = "https://files.pythonhosted.org/packages/82/13/13fa685ae167bee5d94b415991c4fc7bb0a1b6ebea6e753a87044b209678/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fc2ace710ba7c1dfd1a3b42530b62b9ceed115f19a1656adefce7b1782a37794", size = 2269132 }, + { url = "https://files.pythonhosted.org/packages/ef/92/bb7c9395489b99a6cb41d502d3686bac692586db2045adc19e45ee64ed23/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:3452046c37c7692bd52b0e752b87954ef86ee2224e624ef7ce6cb21e8c41cc1b", size = 2425997 }, + { url = "https://files.pythonhosted.org/packages/ed/12/87f0e9271e2b63d35d0d8524954145837dd1a6c15b62a2d8c1ebe0f182b4/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7e9a60b50fe8b2ec6f448fe8d81b07e40141bfced7f896309df271a0b92f80f3", size = 2376060 }, + { url = "https://files.pythonhosted.org/packages/02/6e/c8af39288edbce8bf0fa35dee427b082758a4b71e9c91ef18fa667782138/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:918139571133f366e8362fa4a297aeba86c7816b7ecf0bc79168080e2bd79957", size = 2520471 }, + { url = "https://files.pythonhosted.org/packages/13/78/df381bc7b26e535c91469f77f16adcd073beb3e2dd25042efd064af82323/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e063ef9f89885a1d68dd8b2e18f5ead48653176d10a0e324e3b0030e3a69adeb", size = 2338793 }, + { url = "https://files.pythonhosted.org/packages/d0/dc/c1abe38c37c071d0fc71c9a474fd0b9ede05d42f5a458d584619cfd2371a/kiwisolver-1.4.8-cp313-cp313-win_amd64.whl", hash = "sha256:a17b7c4f5b2c51bb68ed379defd608a03954a1845dfed7cc0117f1cc8a9b7fd2", size = 71855 }, + { url = "https://files.pythonhosted.org/packages/a0/b6/21529d595b126ac298fdd90b705d87d4c5693de60023e0efcb4f387ed99e/kiwisolver-1.4.8-cp313-cp313-win_arm64.whl", hash = "sha256:3cd3bc628b25f74aedc6d374d5babf0166a92ff1317f46267f12d2ed54bc1d30", size = 65430 }, + { url = "https://files.pythonhosted.org/packages/34/bd/b89380b7298e3af9b39f49334e3e2a4af0e04819789f04b43d560516c0c8/kiwisolver-1.4.8-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:370fd2df41660ed4e26b8c9d6bbcad668fbe2560462cba151a721d49e5b6628c", size = 126294 }, + { url = "https://files.pythonhosted.org/packages/83/41/5857dc72e5e4148eaac5aa76e0703e594e4465f8ab7ec0fc60e3a9bb8fea/kiwisolver-1.4.8-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:84a2f830d42707de1d191b9490ac186bf7997a9495d4e9072210a1296345f7dc", size = 67736 }, + { url = "https://files.pythonhosted.org/packages/e1/d1/be059b8db56ac270489fb0b3297fd1e53d195ba76e9bbb30e5401fa6b759/kiwisolver-1.4.8-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7a3ad337add5148cf51ce0b55642dc551c0b9d6248458a757f98796ca7348712", size = 66194 }, + { url = "https://files.pythonhosted.org/packages/e1/83/4b73975f149819eb7dcf9299ed467eba068ecb16439a98990dcb12e63fdd/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7506488470f41169b86d8c9aeff587293f530a23a23a49d6bc64dab66bedc71e", size = 1465942 }, + { url = "https://files.pythonhosted.org/packages/c7/2c/30a5cdde5102958e602c07466bce058b9d7cb48734aa7a4327261ac8e002/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f0121b07b356a22fb0414cec4666bbe36fd6d0d759db3d37228f496ed67c880", size = 1595341 }, + { url = "https://files.pythonhosted.org/packages/ff/9b/1e71db1c000385aa069704f5990574b8244cce854ecd83119c19e83c9586/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d6d6bd87df62c27d4185de7c511c6248040afae67028a8a22012b010bc7ad062", size = 1598455 }, + { url = "https://files.pythonhosted.org/packages/85/92/c8fec52ddf06231b31cbb779af77e99b8253cd96bd135250b9498144c78b/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:291331973c64bb9cce50bbe871fb2e675c4331dab4f31abe89f175ad7679a4d7", size = 1522138 }, + { url = "https://files.pythonhosted.org/packages/0b/51/9eb7e2cd07a15d8bdd976f6190c0164f92ce1904e5c0c79198c4972926b7/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:893f5525bb92d3d735878ec00f781b2de998333659507d29ea4466208df37bed", size = 1582857 }, + { url = "https://files.pythonhosted.org/packages/0f/95/c5a00387a5405e68ba32cc64af65ce881a39b98d73cc394b24143bebc5b8/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b47a465040146981dc9db8647981b8cb96366fbc8d452b031e4f8fdffec3f26d", size = 2293129 }, + { url = "https://files.pythonhosted.org/packages/44/83/eeb7af7d706b8347548313fa3a3a15931f404533cc54fe01f39e830dd231/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:99cea8b9dd34ff80c521aef46a1dddb0dcc0283cf18bde6d756f1e6f31772165", size = 2421538 }, + { url = "https://files.pythonhosted.org/packages/05/f9/27e94c1b3eb29e6933b6986ffc5fa1177d2cd1f0c8efc5f02c91c9ac61de/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:151dffc4865e5fe6dafce5480fab84f950d14566c480c08a53c663a0020504b6", size = 2390661 }, + { url = "https://files.pythonhosted.org/packages/d9/d4/3c9735faa36ac591a4afcc2980d2691000506050b7a7e80bcfe44048daa7/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:577facaa411c10421314598b50413aa1ebcf5126f704f1e5d72d7e4e9f020d90", size = 2546710 }, + { url = "https://files.pythonhosted.org/packages/4c/fa/be89a49c640930180657482a74970cdcf6f7072c8d2471e1babe17a222dc/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:be4816dc51c8a471749d664161b434912eee82f2ea66bd7628bd14583a833e85", size = 2349213 }, + { url = "https://files.pythonhosted.org/packages/1f/f9/ae81c47a43e33b93b0a9819cac6723257f5da2a5a60daf46aa5c7226ea85/kiwisolver-1.4.8-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:e7a019419b7b510f0f7c9dceff8c5eae2392037eae483a7f9162625233802b0a", size = 60403 }, + { url = "https://files.pythonhosted.org/packages/58/ca/f92b5cb6f4ce0c1ebfcfe3e2e42b96917e16f7090e45b21102941924f18f/kiwisolver-1.4.8-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:286b18e86682fd2217a48fc6be6b0f20c1d0ed10958d8dc53453ad58d7be0bf8", size = 58657 }, + { url = "https://files.pythonhosted.org/packages/80/28/ae0240f732f0484d3a4dc885d055653c47144bdf59b670aae0ec3c65a7c8/kiwisolver-1.4.8-pp310-pypy310_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4191ee8dfd0be1c3666ccbac178c5a05d5f8d689bbe3fc92f3c4abec817f8fe0", size = 84948 }, + { url = "https://files.pythonhosted.org/packages/5d/eb/78d50346c51db22c7203c1611f9b513075f35c4e0e4877c5dde378d66043/kiwisolver-1.4.8-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7cd2785b9391f2873ad46088ed7599a6a71e762e1ea33e87514b1a441ed1da1c", size = 81186 }, + { url = "https://files.pythonhosted.org/packages/43/f8/7259f18c77adca88d5f64f9a522792e178b2691f3748817a8750c2d216ef/kiwisolver-1.4.8-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c07b29089b7ba090b6f1a669f1411f27221c3662b3a1b7010e67b59bb5a6f10b", size = 80279 }, + { url = "https://files.pythonhosted.org/packages/3a/1d/50ad811d1c5dae091e4cf046beba925bcae0a610e79ae4c538f996f63ed5/kiwisolver-1.4.8-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:65ea09a5a3faadd59c2ce96dc7bf0f364986a315949dc6374f04396b0d60e09b", size = 71762 }, +] + +[[package]] +name = "llvmlite" +version = "0.43.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/3d/f513755f285db51ab363a53e898b85562e950f79a2e6767a364530c2f645/llvmlite-0.43.0.tar.gz", hash = "sha256:ae2b5b5c3ef67354824fb75517c8db5fbe93bc02cd9671f3c62271626bc041d5", size = 157069 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/ff/6ca7e98998b573b4bd6566f15c35e5c8bea829663a6df0c7aa55ab559da9/llvmlite-0.43.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a289af9a1687c6cf463478f0fa8e8aa3b6fb813317b0d70bf1ed0759eab6f761", size = 31064408 }, + { url = "https://files.pythonhosted.org/packages/ca/5c/a27f9257f86f0cda3f764ff21d9f4217b9f6a0d45e7a39ecfa7905f524ce/llvmlite-0.43.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6d4fd101f571a31acb1559ae1af30f30b1dc4b3186669f92ad780e17c81e91bc", size = 28793153 }, + { url = "https://files.pythonhosted.org/packages/7e/3c/4410f670ad0a911227ea2ecfcba9f672a77cf1924df5280c4562032ec32d/llvmlite-0.43.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7d434ec7e2ce3cc8f452d1cd9a28591745de022f931d67be688a737320dfcead", size = 42857276 }, + { url = "https://files.pythonhosted.org/packages/c6/21/2ffbab5714e72f2483207b4a1de79b2eecd9debbf666ff4e7067bcc5c134/llvmlite-0.43.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6912a87782acdff6eb8bf01675ed01d60ca1f2551f8176a300a886f09e836a6a", size = 43871781 }, + { url = "https://files.pythonhosted.org/packages/f2/26/b5478037c453554a61625ef1125f7e12bb1429ae11c6376f47beba9b0179/llvmlite-0.43.0-cp310-cp310-win_amd64.whl", hash = "sha256:14f0e4bf2fd2d9a75a3534111e8ebeb08eda2f33e9bdd6dfa13282afacdde0ed", size = 28123487 }, + { url = "https://files.pythonhosted.org/packages/95/8c/de3276d773ab6ce3ad676df5fab5aac19696b2956319d65d7dd88fb10f19/llvmlite-0.43.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3e8d0618cb9bfe40ac38a9633f2493d4d4e9fcc2f438d39a4e854f39cc0f5f98", size = 31064409 }, + { url = "https://files.pythonhosted.org/packages/ee/e1/38deed89ced4cf378c61e232265cfe933ccde56ae83c901aa68b477d14b1/llvmlite-0.43.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0a9a1a39d4bf3517f2af9d23d479b4175ead205c592ceeb8b89af48a327ea57", size = 28793149 }, + { url = "https://files.pythonhosted.org/packages/2f/b2/4429433eb2dc8379e2cb582502dca074c23837f8fd009907f78a24de4c25/llvmlite-0.43.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1da416ab53e4f7f3bc8d4eeba36d801cc1894b9fbfbf2022b29b6bad34a7df2", size = 42857277 }, + { url = "https://files.pythonhosted.org/packages/6b/99/5d00a7d671b1ba1751fc9f19d3b36f3300774c6eebe2bcdb5f6191763eb4/llvmlite-0.43.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:977525a1e5f4059316b183fb4fd34fa858c9eade31f165427a3977c95e3ee749", size = 43871781 }, + { url = "https://files.pythonhosted.org/packages/20/ab/ed5ed3688c6ba4f0b8d789da19fd8e30a9cf7fc5852effe311bc5aefe73e/llvmlite-0.43.0-cp311-cp311-win_amd64.whl", hash = "sha256:d5bd550001d26450bd90777736c69d68c487d17bf371438f975229b2b8241a91", size = 28107433 }, + { url = "https://files.pythonhosted.org/packages/0b/67/9443509e5d2b6d8587bae3ede5598fa8bd586b1c7701696663ea8af15b5b/llvmlite-0.43.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f99b600aa7f65235a5a05d0b9a9f31150c390f31261f2a0ba678e26823ec38f7", size = 31064409 }, + { url = "https://files.pythonhosted.org/packages/a2/9c/24139d3712d2d352e300c39c0e00d167472c08b3bd350c3c33d72c88ff8d/llvmlite-0.43.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:35d80d61d0cda2d767f72de99450766250560399edc309da16937b93d3b676e7", size = 28793145 }, + { url = "https://files.pythonhosted.org/packages/bf/f1/4c205a48488e574ee9f6505d50e84370a978c90f08dab41a42d8f2c576b6/llvmlite-0.43.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eccce86bba940bae0d8d48ed925f21dbb813519169246e2ab292b5092aba121f", size = 42857276 }, + { url = "https://files.pythonhosted.org/packages/00/5f/323c4d56e8401c50185fd0e875fcf06b71bf825a863699be1eb10aa2a9cb/llvmlite-0.43.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df6509e1507ca0760787a199d19439cc887bfd82226f5af746d6977bd9f66844", size = 43871781 }, + { url = "https://files.pythonhosted.org/packages/c6/94/dea10e263655ce78d777e78d904903faae39d1fc440762be4a9dc46bed49/llvmlite-0.43.0-cp312-cp312-win_amd64.whl", hash = "sha256:7a2872ee80dcf6b5dbdc838763d26554c2a18aa833d31a2635bff16aafefb9c9", size = 28107442 }, + { url = "https://files.pythonhosted.org/packages/2a/73/12925b1bbb3c2beb6d96f892ef5b4d742c34f00ddb9f4a125e9e87b22f52/llvmlite-0.43.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9cd2a7376f7b3367019b664c21f0c61766219faa3b03731113ead75107f3b66c", size = 31064410 }, + { url = "https://files.pythonhosted.org/packages/cc/61/58c70aa0808a8cba825a7d98cc65bef4801b99328fba80837bfcb5fc767f/llvmlite-0.43.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:18e9953c748b105668487b7c81a3e97b046d8abf95c4ddc0cd3c94f4e4651ae8", size = 28793145 }, + { url = "https://files.pythonhosted.org/packages/c8/c6/9324eb5de2ba9d99cbed853d85ba7a318652a48e077797bec27cf40f911d/llvmlite-0.43.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:74937acd22dc11b33946b67dca7680e6d103d6e90eeaaaf932603bec6fe7b03a", size = 42857276 }, + { url = "https://files.pythonhosted.org/packages/e0/d0/889e9705107db7b1ec0767b03f15d7b95b4c4f9fdf91928ab1c7e9ffacf6/llvmlite-0.43.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc9efc739cc6ed760f795806f67889923f7274276f0eb45092a1473e40d9b867", size = 43871777 }, + { url = "https://files.pythonhosted.org/packages/df/41/73cc26a2634b538cfe813f618c91e7e9960b8c163f8f0c94a2b0f008b9da/llvmlite-0.43.0-cp39-cp39-win_amd64.whl", hash = "sha256:47e147cdda9037f94b399bf03bfd8a6b6b1f2f90be94a454e3386f006455a9b4", size = 28123489 }, +] + +[[package]] +name = "llvmlite" +version = "0.44.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/89/6a/95a3d3610d5c75293d5dbbb2a76480d5d4eeba641557b69fe90af6c5b84e/llvmlite-0.44.0.tar.gz", hash = "sha256:07667d66a5d150abed9157ab6c0b9393c9356f229784a4385c02f99e94fc94d4", size = 171880 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/75/d4863ddfd8ab5f6e70f4504cf8cc37f4e986ec6910f4ef8502bb7d3c1c71/llvmlite-0.44.0-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:9fbadbfba8422123bab5535b293da1cf72f9f478a65645ecd73e781f962ca614", size = 28132306 }, + { url = "https://files.pythonhosted.org/packages/37/d9/6e8943e1515d2f1003e8278819ec03e4e653e2eeb71e4d00de6cfe59424e/llvmlite-0.44.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cccf8eb28f24840f2689fb1a45f9c0f7e582dd24e088dcf96e424834af11f791", size = 26201096 }, + { url = "https://files.pythonhosted.org/packages/aa/46/8ffbc114def88cc698906bf5acab54ca9fdf9214fe04aed0e71731fb3688/llvmlite-0.44.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7202b678cdf904823c764ee0fe2dfe38a76981f4c1e51715b4cb5abb6cf1d9e8", size = 42361859 }, + { url = "https://files.pythonhosted.org/packages/30/1c/9366b29ab050a726af13ebaae8d0dff00c3c58562261c79c635ad4f5eb71/llvmlite-0.44.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40526fb5e313d7b96bda4cbb2c85cd5374e04d80732dd36a282d72a560bb6408", size = 41184199 }, + { url = "https://files.pythonhosted.org/packages/69/07/35e7c594b021ecb1938540f5bce543ddd8713cff97f71d81f021221edc1b/llvmlite-0.44.0-cp310-cp310-win_amd64.whl", hash = "sha256:41e3839150db4330e1b2716c0be3b5c4672525b4c9005e17c7597f835f351ce2", size = 30332381 }, + { url = "https://files.pythonhosted.org/packages/b5/e2/86b245397052386595ad726f9742e5223d7aea999b18c518a50e96c3aca4/llvmlite-0.44.0-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:eed7d5f29136bda63b6d7804c279e2b72e08c952b7c5df61f45db408e0ee52f3", size = 28132305 }, + { url = "https://files.pythonhosted.org/packages/ff/ec/506902dc6870249fbe2466d9cf66d531265d0f3a1157213c8f986250c033/llvmlite-0.44.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ace564d9fa44bb91eb6e6d8e7754977783c68e90a471ea7ce913bff30bd62427", size = 26201090 }, + { url = "https://files.pythonhosted.org/packages/99/fe/d030f1849ebb1f394bb3f7adad5e729b634fb100515594aca25c354ffc62/llvmlite-0.44.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c5d22c3bfc842668168a786af4205ec8e3ad29fb1bc03fd11fd48460d0df64c1", size = 42361858 }, + { url = "https://files.pythonhosted.org/packages/d7/7a/ce6174664b9077fc673d172e4c888cb0b128e707e306bc33fff8c2035f0d/llvmlite-0.44.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f01a394e9c9b7b1d4e63c327b096d10f6f0ed149ef53d38a09b3749dcf8c9610", size = 41184200 }, + { url = "https://files.pythonhosted.org/packages/5f/c6/258801143975a6d09a373f2641237992496e15567b907a4d401839d671b8/llvmlite-0.44.0-cp311-cp311-win_amd64.whl", hash = "sha256:d8489634d43c20cd0ad71330dde1d5bc7b9966937a263ff1ec1cebb90dc50955", size = 30331193 }, + { url = "https://files.pythonhosted.org/packages/15/86/e3c3195b92e6e492458f16d233e58a1a812aa2bfbef9bdd0fbafcec85c60/llvmlite-0.44.0-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:1d671a56acf725bf1b531d5ef76b86660a5ab8ef19bb6a46064a705c6ca80aad", size = 28132297 }, + { url = "https://files.pythonhosted.org/packages/d6/53/373b6b8be67b9221d12b24125fd0ec56b1078b660eeae266ec388a6ac9a0/llvmlite-0.44.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5f79a728e0435493611c9f405168682bb75ffd1fbe6fc360733b850c80a026db", size = 26201105 }, + { url = "https://files.pythonhosted.org/packages/cb/da/8341fd3056419441286c8e26bf436923021005ece0bff5f41906476ae514/llvmlite-0.44.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0143a5ef336da14deaa8ec26c5449ad5b6a2b564df82fcef4be040b9cacfea9", size = 42361901 }, + { url = "https://files.pythonhosted.org/packages/53/ad/d79349dc07b8a395a99153d7ce8b01d6fcdc9f8231355a5df55ded649b61/llvmlite-0.44.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d752f89e31b66db6f8da06df8b39f9b91e78c5feea1bf9e8c1fba1d1c24c065d", size = 41184247 }, + { url = "https://files.pythonhosted.org/packages/e2/3b/a9a17366af80127bd09decbe2a54d8974b6d8b274b39bf47fbaedeec6307/llvmlite-0.44.0-cp312-cp312-win_amd64.whl", hash = "sha256:eae7e2d4ca8f88f89d315b48c6b741dcb925d6a1042da694aa16ab3dd4cbd3a1", size = 30332380 }, + { url = "https://files.pythonhosted.org/packages/89/24/4c0ca705a717514c2092b18476e7a12c74d34d875e05e4d742618ebbf449/llvmlite-0.44.0-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:319bddd44e5f71ae2689859b7203080716448a3cd1128fb144fe5c055219d516", size = 28132306 }, + { url = "https://files.pythonhosted.org/packages/01/cf/1dd5a60ba6aee7122ab9243fd614abcf22f36b0437cbbe1ccf1e3391461c/llvmlite-0.44.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9c58867118bad04a0bb22a2e0068c693719658105e40009ffe95c7000fcde88e", size = 26201090 }, + { url = "https://files.pythonhosted.org/packages/d2/1b/656f5a357de7135a3777bd735cc7c9b8f23b4d37465505bd0eaf4be9befe/llvmlite-0.44.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46224058b13c96af1365290bdfebe9a6264ae62fb79b2b55693deed11657a8bf", size = 42361904 }, + { url = "https://files.pythonhosted.org/packages/d8/e1/12c5f20cb9168fb3464a34310411d5ad86e4163c8ff2d14a2b57e5cc6bac/llvmlite-0.44.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa0097052c32bf721a4efc03bd109d335dfa57d9bffb3d4c24cc680711b8b4fc", size = 41184245 }, + { url = "https://files.pythonhosted.org/packages/d0/81/e66fc86539293282fd9cb7c9417438e897f369e79ffb62e1ae5e5154d4dd/llvmlite-0.44.0-cp313-cp313-win_amd64.whl", hash = "sha256:2fb7c4f2fb86cbae6dca3db9ab203eeea0e22d73b99bc2341cdf9de93612e930", size = 30331193 }, +] + +[[package]] +name = "matplotlib" +version = "3.9.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "contourpy", version = "1.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "cycler", marker = "python_full_version < '3.10'" }, + { name = "fonttools", marker = "python_full_version < '3.10'" }, + { name = "importlib-resources", marker = "python_full_version < '3.10'" }, + { name = "kiwisolver", version = "1.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "packaging", marker = "python_full_version < '3.10'" }, + { name = "pillow", marker = "python_full_version < '3.10'" }, + { name = "pyparsing", marker = "python_full_version < '3.10'" }, + { name = "python-dateutil", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/17/1747b4154034befd0ed33b52538f5eb7752d05bb51c5e2a31470c3bc7d52/matplotlib-3.9.4.tar.gz", hash = "sha256:1e00e8be7393cbdc6fedfa8a6fba02cf3e83814b285db1c60b906a023ba41bc3", size = 36106529 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/94/27d2e2c30d54b56c7b764acc1874a909e34d1965a427fc7092bb6a588b63/matplotlib-3.9.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:c5fdd7abfb706dfa8d307af64a87f1a862879ec3cd8d0ec8637458f0885b9c50", size = 7885089 }, + { url = "https://files.pythonhosted.org/packages/c6/25/828273307e40a68eb8e9df832b6b2aaad075864fdc1de4b1b81e40b09e48/matplotlib-3.9.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d89bc4e85e40a71d1477780366c27fb7c6494d293e1617788986f74e2a03d7ff", size = 7770600 }, + { url = "https://files.pythonhosted.org/packages/f2/65/f841a422ec994da5123368d76b126acf4fc02ea7459b6e37c4891b555b83/matplotlib-3.9.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ddf9f3c26aae695c5daafbf6b94e4c1a30d6cd617ba594bbbded3b33a1fcfa26", size = 8200138 }, + { url = "https://files.pythonhosted.org/packages/07/06/272aca07a38804d93b6050813de41ca7ab0e29ba7a9dd098e12037c919a9/matplotlib-3.9.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18ebcf248030173b59a868fda1fe42397253f6698995b55e81e1f57431d85e50", size = 8312711 }, + { url = "https://files.pythonhosted.org/packages/98/37/f13e23b233c526b7e27ad61be0a771894a079e0f7494a10d8d81557e0e9a/matplotlib-3.9.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:974896ec43c672ec23f3f8c648981e8bc880ee163146e0312a9b8def2fac66f5", size = 9090622 }, + { url = "https://files.pythonhosted.org/packages/4f/8c/b1f5bd2bd70e60f93b1b54c4d5ba7a992312021d0ddddf572f9a1a6d9348/matplotlib-3.9.4-cp310-cp310-win_amd64.whl", hash = "sha256:4598c394ae9711cec135639374e70871fa36b56afae17bdf032a345be552a88d", size = 7828211 }, + { url = "https://files.pythonhosted.org/packages/74/4b/65be7959a8fa118a3929b49a842de5b78bb55475236fcf64f3e308ff74a0/matplotlib-3.9.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d4dd29641d9fb8bc4492420c5480398dd40a09afd73aebe4eb9d0071a05fbe0c", size = 7894430 }, + { url = "https://files.pythonhosted.org/packages/e9/18/80f70d91896e0a517b4a051c3fd540daa131630fd75e02e250365353b253/matplotlib-3.9.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30e5b22e8bcfb95442bf7d48b0d7f3bdf4a450cbf68986ea45fca3d11ae9d099", size = 7780045 }, + { url = "https://files.pythonhosted.org/packages/a2/73/ccb381026e3238c5c25c3609ba4157b2d1a617ec98d65a8b4ee4e1e74d02/matplotlib-3.9.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bb0030d1d447fd56dcc23b4c64a26e44e898f0416276cac1ebc25522e0ac249", size = 8209906 }, + { url = "https://files.pythonhosted.org/packages/ab/33/1648da77b74741c89f5ea95cbf42a291b4b364f2660b316318811404ed97/matplotlib-3.9.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aca90ed222ac3565d2752b83dbb27627480d27662671e4d39da72e97f657a423", size = 8322873 }, + { url = "https://files.pythonhosted.org/packages/57/d3/8447ba78bc6593c9044c372d1609f8ea10fb1e071e7a9e0747bea74fc16c/matplotlib-3.9.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a181b2aa2906c608fcae72f977a4a2d76e385578939891b91c2550c39ecf361e", size = 9099566 }, + { url = "https://files.pythonhosted.org/packages/23/e1/4f0e237bf349c02ff9d1b6e7109f1a17f745263809b9714a8576dc17752b/matplotlib-3.9.4-cp311-cp311-win_amd64.whl", hash = "sha256:1f6882828231eca17f501c4dcd98a05abb3f03d157fbc0769c6911fe08b6cfd3", size = 7838065 }, + { url = "https://files.pythonhosted.org/packages/1a/2b/c918bf6c19d6445d1cefe3d2e42cb740fb997e14ab19d4daeb6a7ab8a157/matplotlib-3.9.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:dfc48d67e6661378a21c2983200a654b72b5c5cdbd5d2cf6e5e1ece860f0cc70", size = 7891131 }, + { url = "https://files.pythonhosted.org/packages/c1/e5/b4e8fc601ca302afeeabf45f30e706a445c7979a180e3a978b78b2b681a4/matplotlib-3.9.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:47aef0fab8332d02d68e786eba8113ffd6f862182ea2999379dec9e237b7e483", size = 7776365 }, + { url = "https://files.pythonhosted.org/packages/99/06/b991886c506506476e5d83625c5970c656a491b9f80161458fed94597808/matplotlib-3.9.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fba1f52c6b7dc764097f52fd9ab627b90db452c9feb653a59945de16752e965f", size = 8200707 }, + { url = "https://files.pythonhosted.org/packages/c3/e2/556b627498cb27e61026f2d1ba86a78ad1b836fef0996bef5440e8bc9559/matplotlib-3.9.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:173ac3748acaac21afcc3fa1633924609ba1b87749006bc25051c52c422a5d00", size = 8313761 }, + { url = "https://files.pythonhosted.org/packages/58/ff/165af33ec766ff818306ea88e91f9f60d2a6ed543be1eb122a98acbf3b0d/matplotlib-3.9.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:320edea0cadc07007765e33f878b13b3738ffa9745c5f707705692df70ffe0e0", size = 9095284 }, + { url = "https://files.pythonhosted.org/packages/9f/8b/3d0c7a002db3b1ed702731c2a9a06d78d035f1f2fb0fb936a8e43cc1e9f4/matplotlib-3.9.4-cp312-cp312-win_amd64.whl", hash = "sha256:a4a4cfc82330b27042a7169533da7991e8789d180dd5b3daeaee57d75cd5a03b", size = 7841160 }, + { url = "https://files.pythonhosted.org/packages/49/b1/999f89a7556d101b23a2f0b54f1b6e140d73f56804da1398f2f0bc0924bc/matplotlib-3.9.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:37eeffeeca3c940985b80f5b9a7b95ea35671e0e7405001f249848d2b62351b6", size = 7891499 }, + { url = "https://files.pythonhosted.org/packages/87/7b/06a32b13a684977653396a1bfcd34d4e7539c5d55c8cbfaa8ae04d47e4a9/matplotlib-3.9.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3e7465ac859ee4abcb0d836137cd8414e7bb7ad330d905abced457217d4f0f45", size = 7776802 }, + { url = "https://files.pythonhosted.org/packages/65/87/ac498451aff739e515891bbb92e566f3c7ef31891aaa878402a71f9b0910/matplotlib-3.9.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4c12302c34afa0cf061bea23b331e747e5e554b0fa595c96e01c7b75bc3b858", size = 8200802 }, + { url = "https://files.pythonhosted.org/packages/f8/6b/9eb761c00e1cb838f6c92e5f25dcda3f56a87a52f6cb8fdfa561e6cf6a13/matplotlib-3.9.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b8c97917f21b75e72108b97707ba3d48f171541a74aa2a56df7a40626bafc64", size = 8313880 }, + { url = "https://files.pythonhosted.org/packages/d7/a2/c8eaa600e2085eec7e38cbbcc58a30fc78f8224939d31d3152bdafc01fd1/matplotlib-3.9.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0229803bd7e19271b03cb09f27db76c918c467aa4ce2ae168171bc67c3f508df", size = 9094637 }, + { url = "https://files.pythonhosted.org/packages/71/1f/c6e1daea55b7bfeb3d84c6cb1abc449f6a02b181e7e2a5e4db34c3afb793/matplotlib-3.9.4-cp313-cp313-win_amd64.whl", hash = "sha256:7c0d8ef442ebf56ff5e206f8083d08252ee738e04f3dc88ea882853a05488799", size = 7841311 }, + { url = "https://files.pythonhosted.org/packages/c0/3a/2757d3f7d388b14dd48f5a83bea65b6d69f000e86b8f28f74d86e0d375bd/matplotlib-3.9.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a04c3b00066a688834356d196136349cb32f5e1003c55ac419e91585168b88fb", size = 7919989 }, + { url = "https://files.pythonhosted.org/packages/24/28/f5077c79a4f521589a37fe1062d6a6ea3534e068213f7357e7cfffc2e17a/matplotlib-3.9.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:04c519587f6c210626741a1e9a68eefc05966ede24205db8982841826af5871a", size = 7809417 }, + { url = "https://files.pythonhosted.org/packages/36/c8/c523fd2963156692916a8eb7d4069084cf729359f7955cf09075deddfeaf/matplotlib-3.9.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:308afbf1a228b8b525fcd5cec17f246bbbb63b175a3ef6eb7b4d33287ca0cf0c", size = 8226258 }, + { url = "https://files.pythonhosted.org/packages/f6/88/499bf4b8fa9349b6f5c0cf4cead0ebe5da9d67769129f1b5651e5ac51fbc/matplotlib-3.9.4-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ddb3b02246ddcffd3ce98e88fed5b238bc5faff10dbbaa42090ea13241d15764", size = 8335849 }, + { url = "https://files.pythonhosted.org/packages/b8/9f/20a4156b9726188646a030774ee337d5ff695a965be45ce4dbcb9312c170/matplotlib-3.9.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8a75287e9cb9eee48cb79ec1d806f75b29c0fde978cb7223a1f4c5848d696041", size = 9102152 }, + { url = "https://files.pythonhosted.org/packages/10/11/237f9c3a4e8d810b1759b67ff2da7c32c04f9c80aa475e7beb36ed43a8fb/matplotlib-3.9.4-cp313-cp313t-win_amd64.whl", hash = "sha256:488deb7af140f0ba86da003e66e10d55ff915e152c78b4b66d231638400b1965", size = 7896987 }, + { url = "https://files.pythonhosted.org/packages/56/eb/501b465c9fef28f158e414ea3a417913dc2ac748564c7ed41535f23445b4/matplotlib-3.9.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:3c3724d89a387ddf78ff88d2a30ca78ac2b4c89cf37f2db4bd453c34799e933c", size = 7885919 }, + { url = "https://files.pythonhosted.org/packages/da/36/236fbd868b6c91309a5206bd90c3f881f4f44b2d997cd1d6239ef652f878/matplotlib-3.9.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d5f0a8430ffe23d7e32cfd86445864ccad141797f7d25b7c41759a5b5d17cfd7", size = 7771486 }, + { url = "https://files.pythonhosted.org/packages/e0/4b/105caf2d54d5ed11d9f4335398f5103001a03515f2126c936a752ccf1461/matplotlib-3.9.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6bb0141a21aef3b64b633dc4d16cbd5fc538b727e4958be82a0e1c92a234160e", size = 8201838 }, + { url = "https://files.pythonhosted.org/packages/5d/a7/bb01188fb4013d34d274caf44a2f8091255b0497438e8b6c0a7c1710c692/matplotlib-3.9.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:57aa235109e9eed52e2c2949db17da185383fa71083c00c6c143a60e07e0888c", size = 8314492 }, + { url = "https://files.pythonhosted.org/packages/33/19/02e1a37f7141fc605b193e927d0a9cdf9dc124a20b9e68793f4ffea19695/matplotlib-3.9.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b18c600061477ccfdd1e6fd050c33d8be82431700f3452b297a56d9ed7037abb", size = 9092500 }, + { url = "https://files.pythonhosted.org/packages/57/68/c2feb4667adbf882ffa4b3e0ac9967f848980d9f8b5bebd86644aa67ce6a/matplotlib-3.9.4-cp39-cp39-win_amd64.whl", hash = "sha256:ef5f2d1b67d2d2145ff75e10f8c008bfbf71d45137c4b648c87193e7dd053eac", size = 7822962 }, + { url = "https://files.pythonhosted.org/packages/0c/22/2ef6a364cd3f565442b0b055e0599744f1e4314ec7326cdaaa48a4d864d7/matplotlib-3.9.4-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:44e0ed786d769d85bc787b0606a53f2d8d2d1d3c8a2608237365e9121c1a338c", size = 7877995 }, + { url = "https://files.pythonhosted.org/packages/87/b8/2737456e566e9f4d94ae76b8aa0d953d9acb847714f9a7ad80184474f5be/matplotlib-3.9.4-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:09debb9ce941eb23ecdbe7eab972b1c3e0276dcf01688073faff7b0f61d6c6ca", size = 7769300 }, + { url = "https://files.pythonhosted.org/packages/b2/1f/e709c6ec7b5321e6568769baa288c7178e60a93a9da9e682b39450da0e29/matplotlib-3.9.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bcc53cf157a657bfd03afab14774d54ba73aa84d42cfe2480c91bd94873952db", size = 8313423 }, + { url = "https://files.pythonhosted.org/packages/5e/b6/5a1f868782cd13f053a679984e222007ecff654a9bfbac6b27a65f4eeb05/matplotlib-3.9.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:ad45da51be7ad02387801fd154ef74d942f49fe3fcd26a64c94842ba7ec0d865", size = 7854624 }, +] + +[[package]] +name = "matplotlib" +version = "3.10.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "contourpy", version = "1.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "cycler", marker = "python_full_version >= '3.10'" }, + { name = "fonttools", marker = "python_full_version >= '3.10'" }, + { name = "kiwisolver", version = "1.4.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "packaging", marker = "python_full_version >= '3.10'" }, + { name = "pillow", marker = "python_full_version >= '3.10'" }, + { name = "pyparsing", marker = "python_full_version >= '3.10'" }, + { name = "python-dateutil", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/26/91/d49359a21893183ed2a5b6c76bec40e0b1dcbf8ca148f864d134897cfc75/matplotlib-3.10.3.tar.gz", hash = "sha256:2f82d2c5bb7ae93aaaa4cd42aca65d76ce6376f83304fa3a630b569aca274df0", size = 34799811 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/ea/2bba25d289d389c7451f331ecd593944b3705f06ddf593fa7be75037d308/matplotlib-3.10.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:213fadd6348d106ca7db99e113f1bea1e65e383c3ba76e8556ba4a3054b65ae7", size = 8167862 }, + { url = "https://files.pythonhosted.org/packages/41/81/cc70b5138c926604e8c9ed810ed4c79e8116ba72e02230852f5c12c87ba2/matplotlib-3.10.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d3bec61cb8221f0ca6313889308326e7bb303d0d302c5cc9e523b2f2e6c73deb", size = 8042149 }, + { url = "https://files.pythonhosted.org/packages/4a/9a/0ff45b6bfa42bb16de597e6058edf2361c298ad5ef93b327728145161bbf/matplotlib-3.10.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c21ae75651c0231b3ba014b6d5e08fb969c40cdb5a011e33e99ed0c9ea86ecb", size = 8453719 }, + { url = "https://files.pythonhosted.org/packages/85/c7/1866e972fed6d71ef136efbc980d4d1854ab7ef1ea8152bbd995ca231c81/matplotlib-3.10.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a49e39755580b08e30e3620efc659330eac5d6534ab7eae50fa5e31f53ee4e30", size = 8590801 }, + { url = "https://files.pythonhosted.org/packages/5d/b9/748f6626d534ab7e255bdc39dc22634d337cf3ce200f261b5d65742044a1/matplotlib-3.10.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cf4636203e1190871d3a73664dea03d26fb019b66692cbfd642faafdad6208e8", size = 9402111 }, + { url = "https://files.pythonhosted.org/packages/1f/78/8bf07bd8fb67ea5665a6af188e70b57fcb2ab67057daa06b85a08e59160a/matplotlib-3.10.3-cp310-cp310-win_amd64.whl", hash = "sha256:fd5641a9bb9d55f4dd2afe897a53b537c834b9012684c8444cc105895c8c16fd", size = 8057213 }, + { url = "https://files.pythonhosted.org/packages/f5/bd/af9f655456f60fe1d575f54fb14704ee299b16e999704817a7645dfce6b0/matplotlib-3.10.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0ef061f74cd488586f552d0c336b2f078d43bc00dc473d2c3e7bfee2272f3fa8", size = 8178873 }, + { url = "https://files.pythonhosted.org/packages/c2/86/e1c86690610661cd716eda5f9d0b35eaf606ae6c9b6736687cfc8f2d0cd8/matplotlib-3.10.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d96985d14dc5f4a736bbea4b9de9afaa735f8a0fc2ca75be2fa9e96b2097369d", size = 8052205 }, + { url = "https://files.pythonhosted.org/packages/54/51/a9f8e49af3883dacddb2da1af5fca1f7468677f1188936452dd9aaaeb9ed/matplotlib-3.10.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7c5f0283da91e9522bdba4d6583ed9d5521566f63729ffb68334f86d0bb98049", size = 8465823 }, + { url = "https://files.pythonhosted.org/packages/e7/e3/c82963a3b86d6e6d5874cbeaa390166458a7f1961bab9feb14d3d1a10f02/matplotlib-3.10.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdfa07c0ec58035242bc8b2c8aae37037c9a886370eef6850703d7583e19964b", size = 8606464 }, + { url = "https://files.pythonhosted.org/packages/0e/34/24da1027e7fcdd9e82da3194c470143c551852757a4b473a09a012f5b945/matplotlib-3.10.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c0b9849a17bce080a16ebcb80a7b714b5677d0ec32161a2cc0a8e5a6030ae220", size = 9413103 }, + { url = "https://files.pythonhosted.org/packages/a6/da/948a017c3ea13fd4a97afad5fdebe2f5bbc4d28c0654510ce6fd6b06b7bd/matplotlib-3.10.3-cp311-cp311-win_amd64.whl", hash = "sha256:eef6ed6c03717083bc6d69c2d7ee8624205c29a8e6ea5a31cd3492ecdbaee1e1", size = 8065492 }, + { url = "https://files.pythonhosted.org/packages/eb/43/6b80eb47d1071f234ef0c96ca370c2ca621f91c12045f1401b5c9b28a639/matplotlib-3.10.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0ab1affc11d1f495ab9e6362b8174a25afc19c081ba5b0775ef00533a4236eea", size = 8179689 }, + { url = "https://files.pythonhosted.org/packages/0f/70/d61a591958325c357204870b5e7b164f93f2a8cca1dc6ce940f563909a13/matplotlib-3.10.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2a818d8bdcafa7ed2eed74487fdb071c09c1ae24152d403952adad11fa3c65b4", size = 8050466 }, + { url = "https://files.pythonhosted.org/packages/e7/75/70c9d2306203148cc7902a961240c5927dd8728afedf35e6a77e105a2985/matplotlib-3.10.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:748ebc3470c253e770b17d8b0557f0aa85cf8c63fd52f1a61af5b27ec0b7ffee", size = 8456252 }, + { url = "https://files.pythonhosted.org/packages/c4/91/ba0ae1ff4b3f30972ad01cd4a8029e70a0ec3b8ea5be04764b128b66f763/matplotlib-3.10.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed70453fd99733293ace1aec568255bc51c6361cb0da94fa5ebf0649fdb2150a", size = 8601321 }, + { url = "https://files.pythonhosted.org/packages/d2/88/d636041eb54a84b889e11872d91f7cbf036b3b0e194a70fa064eb8b04f7a/matplotlib-3.10.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dbed9917b44070e55640bd13419de83b4c918e52d97561544814ba463811cbc7", size = 9406972 }, + { url = "https://files.pythonhosted.org/packages/b1/79/0d1c165eac44405a86478082e225fce87874f7198300bbebc55faaf6d28d/matplotlib-3.10.3-cp312-cp312-win_amd64.whl", hash = "sha256:cf37d8c6ef1a48829443e8ba5227b44236d7fcaf7647caa3178a4ff9f7a5be05", size = 8067954 }, + { url = "https://files.pythonhosted.org/packages/3b/c1/23cfb566a74c696a3b338d8955c549900d18fe2b898b6e94d682ca21e7c2/matplotlib-3.10.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9f2efccc8dcf2b86fc4ee849eea5dcaecedd0773b30f47980dc0cbeabf26ec84", size = 8180318 }, + { url = "https://files.pythonhosted.org/packages/6c/0c/02f1c3b66b30da9ee343c343acbb6251bef5b01d34fad732446eaadcd108/matplotlib-3.10.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3ddbba06a6c126e3301c3d272a99dcbe7f6c24c14024e80307ff03791a5f294e", size = 8051132 }, + { url = "https://files.pythonhosted.org/packages/b4/ab/8db1a5ac9b3a7352fb914133001dae889f9fcecb3146541be46bed41339c/matplotlib-3.10.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:748302b33ae9326995b238f606e9ed840bf5886ebafcb233775d946aa8107a15", size = 8457633 }, + { url = "https://files.pythonhosted.org/packages/f5/64/41c4367bcaecbc03ef0d2a3ecee58a7065d0a36ae1aa817fe573a2da66d4/matplotlib-3.10.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a80fcccbef63302c0efd78042ea3c2436104c5b1a4d3ae20f864593696364ac7", size = 8601031 }, + { url = "https://files.pythonhosted.org/packages/12/6f/6cc79e9e5ab89d13ed64da28898e40fe5b105a9ab9c98f83abd24e46d7d7/matplotlib-3.10.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:55e46cbfe1f8586adb34f7587c3e4f7dedc59d5226719faf6cb54fc24f2fd52d", size = 9406988 }, + { url = "https://files.pythonhosted.org/packages/b1/0f/eed564407bd4d935ffabf561ed31099ed609e19287409a27b6d336848653/matplotlib-3.10.3-cp313-cp313-win_amd64.whl", hash = "sha256:151d89cb8d33cb23345cd12490c76fd5d18a56581a16d950b48c6ff19bb2ab93", size = 8068034 }, + { url = "https://files.pythonhosted.org/packages/3e/e5/2f14791ff69b12b09e9975e1d116d9578ac684460860ce542c2588cb7a1c/matplotlib-3.10.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:c26dd9834e74d164d06433dc7be5d75a1e9890b926b3e57e74fa446e1a62c3e2", size = 8218223 }, + { url = "https://files.pythonhosted.org/packages/5c/08/30a94afd828b6e02d0a52cae4a29d6e9ccfcf4c8b56cc28b021d3588873e/matplotlib-3.10.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:24853dad5b8c84c8c2390fc31ce4858b6df504156893292ce8092d190ef8151d", size = 8094985 }, + { url = "https://files.pythonhosted.org/packages/89/44/f3bc6b53066c889d7a1a3ea8094c13af6a667c5ca6220ec60ecceec2dabe/matplotlib-3.10.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68f7878214d369d7d4215e2a9075fef743be38fa401d32e6020bab2dfabaa566", size = 8483109 }, + { url = "https://files.pythonhosted.org/packages/ba/c7/473bc559beec08ebee9f86ca77a844b65747e1a6c2691e8c92e40b9f42a8/matplotlib-3.10.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6929fc618cb6db9cb75086f73b3219bbb25920cb24cee2ea7a12b04971a4158", size = 8618082 }, + { url = "https://files.pythonhosted.org/packages/d8/e9/6ce8edd264c8819e37bbed8172e0ccdc7107fe86999b76ab5752276357a4/matplotlib-3.10.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6c7818292a5cc372a2dc4c795e5c356942eb8350b98ef913f7fda51fe175ac5d", size = 9413699 }, + { url = "https://files.pythonhosted.org/packages/1b/92/9a45c91089c3cf690b5badd4be81e392ff086ccca8a1d4e3a08463d8a966/matplotlib-3.10.3-cp313-cp313t-win_amd64.whl", hash = "sha256:4f23ffe95c5667ef8a2b56eea9b53db7f43910fa4a2d5472ae0f72b64deab4d5", size = 8139044 }, + { url = "https://files.pythonhosted.org/packages/3d/d1/f54d43e95384b312ffa4a74a4326c722f3b8187aaaa12e9a84cdf3037131/matplotlib-3.10.3-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:86ab63d66bbc83fdb6733471d3bff40897c1e9921cba112accd748eee4bce5e4", size = 8162896 }, + { url = "https://files.pythonhosted.org/packages/24/a4/fbfc00c2346177c95b353dcf9b5a004106abe8730a62cb6f27e79df0a698/matplotlib-3.10.3-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:a48f9c08bf7444b5d2391a83e75edb464ccda3c380384b36532a0962593a1751", size = 8039702 }, + { url = "https://files.pythonhosted.org/packages/6a/b9/59e120d24a2ec5fc2d30646adb2efb4621aab3c6d83d66fb2a7a182db032/matplotlib-3.10.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb73d8aa75a237457988f9765e4dfe1c0d2453c5ca4eabc897d4309672c8e014", size = 8594298 }, +] + +[[package]] +name = "maturin" +version = "1.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/94/f7/73cf2ae0d6db943a627d28c09f5368735fce6b8b2ad1e1f6bcda2632c80a/maturin-1.9.1.tar.gz", hash = "sha256:97b52fb19d20c1fdc70e4efdc05d79853a4c9c0051030c93a793cd5181dc4ccd", size = 209757 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/f2/de43e8954092bd957fbdfbc5b978bf8be40f27aec1a4ebd65e57cfb3ec8a/maturin-1.9.1-py3-none-linux_armv6l.whl", hash = "sha256:fe8f59f9e387fb19635eab6b7381ef718e5dc7a328218e6da604c91f206cbb72", size = 8270244 }, + { url = "https://files.pythonhosted.org/packages/b8/72/36966375c2c2bb2d66df4fa756cfcd54175773719b98d4b26a6b4d1f0bfc/maturin-1.9.1-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:6a9c9d176f6df3a8ec1a4c9c72c8a49674ed13668a03c9ead5fab983bbeeb624", size = 16053959 }, + { url = "https://files.pythonhosted.org/packages/c4/40/4e0da87e563333ff1605fef15bed5858c2a41c0c0404e47f20086f214473/maturin-1.9.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e14eedbc4369dda1347ce9ddc183ade7c513d9975b7ea2b9c9e4211fb74f597a", size = 8407170 }, + { url = "https://files.pythonhosted.org/packages/d9/27/4b29614964c10370effcdfcf34ec57126c9a4b921b7a2c42a94ae3a59cb0/maturin-1.9.1-py3-none-manylinux_2_12_i686.manylinux2010_i686.musllinux_1_1_i686.whl", hash = "sha256:2f05f07bc887e010c44d32a088aea4f36a2104e301f51f408481e4e9759471a7", size = 8258775 }, + { url = "https://files.pythonhosted.org/packages/e0/5b/b15ad53e1e6733d8798ce903d25d9e05aa3083b2544f1a6f863ea01dd50d/maturin-1.9.1-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.musllinux_1_1_x86_64.whl", hash = "sha256:e7eb54db3aace213420cd545b24a149842e8d6b1fcec046d0346f299d8adfc34", size = 8787295 }, + { url = "https://files.pythonhosted.org/packages/72/d8/b97f4767786eae63bb6b700b342766bcea88da98796bfee290bcddd99fd8/maturin-1.9.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:9d037a37b8ef005eebdea61eaf0e3053ebcad3b740162932fbc120db5fdf5653", size = 8053283 }, + { url = "https://files.pythonhosted.org/packages/95/45/770fc005bceac81f5905c96f37c36f65fa9c3da3f4aa8d4e4d2a883aa967/maturin-1.9.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:7c26fb60d80e6a72a8790202bb14dbef956b831044f55d1ce4e2c2e915eb6124", size = 8127120 }, + { url = "https://files.pythonhosted.org/packages/2f/a6/be684b4fce58f8b3a9d3b701c23961d5fe0e1710ed484e2216441997e74f/maturin-1.9.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.musllinux_1_1_ppc64le.whl", hash = "sha256:e0a2c546c123ed97d1ee0c9cc80a912d9174913643c737c12adf4bce46603bb3", size = 10569627 }, + { url = "https://files.pythonhosted.org/packages/24/ad/7f8a9d8a1b79c2ed6291aaaa22147c98efee729b23df2803c319dd658049/maturin-1.9.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f5dde6fbcc36a1173fe74e6629fee36e89df76236247b64b23055f1f820bdf35", size = 8934678 }, + { url = "https://files.pythonhosted.org/packages/59/5f/97ff670cb718a40ee21faf38e07e0d773573180de98ee453142e5f932052/maturin-1.9.1-py3-none-win32.whl", hash = "sha256:69d9f752f33a3c95062014f464cbd715e83a175f4601b76a9ce3db6ea18df976", size = 7261272 }, + { url = "https://files.pythonhosted.org/packages/a6/07/c99058a73d0f7d8e8c87bf60c48a96c44f42ff4ef6a6ae4ca3821605bdd2/maturin-1.9.1-py3-none-win_amd64.whl", hash = "sha256:c8b71cf0f6a5f712ac1466641d520e2ce3fbe44104319a55d875cc8326dcdd61", size = 8280274 }, + { url = "https://files.pythonhosted.org/packages/06/3d/74e75874b75fc82e4774f2ed78ad546fda3e127bae4a971db3611bdab285/maturin-1.9.1-py3-none-win_arm64.whl", hash = "sha256:0e6e2ddc83999ac3999576b06649a327536a51d57c917fa01416e40f53106bda", size = 6936614 }, +] + +[[package]] +name = "mmh3" +version = "5.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/47/1b/1fc6888c74cbd8abad1292dde2ddfcf8fc059e114c97dd6bf16d12f36293/mmh3-5.1.0.tar.gz", hash = "sha256:136e1e670500f177f49ec106a4ebf0adf20d18d96990cc36ea492c651d2b406c", size = 33728 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a1/01/9d06468928661765c0fc248a29580c760a4a53a9c6c52cf72528bae3582e/mmh3-5.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:eaf4ac5c6ee18ca9232238364d7f2a213278ae5ca97897cafaa123fcc7bb8bec", size = 56095 }, + { url = "https://files.pythonhosted.org/packages/e4/d7/7b39307fc9db867b2a9a20c58b0de33b778dd6c55e116af8ea031f1433ba/mmh3-5.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:48f9aa8ccb9ad1d577a16104834ac44ff640d8de8c0caed09a2300df7ce8460a", size = 40512 }, + { url = "https://files.pythonhosted.org/packages/4f/85/728ca68280d8ccc60c113ad119df70ff1748fbd44c89911fed0501faf0b8/mmh3-5.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d4ba8cac21e1f2d4e436ce03a82a7f87cda80378691f760e9ea55045ec480a3d", size = 40110 }, + { url = "https://files.pythonhosted.org/packages/e4/96/beaf0e301472ffa00358bbbf771fe2d9c4d709a2fe30b1d929e569f8cbdf/mmh3-5.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d69281c281cb01994f054d862a6bb02a2e7acfe64917795c58934b0872b9ece4", size = 100151 }, + { url = "https://files.pythonhosted.org/packages/c3/ee/9381f825c4e09ffafeffa213c3865c4bf7d39771640de33ab16f6faeb854/mmh3-5.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4d05ed3962312fbda2a1589b97359d2467f677166952f6bd410d8c916a55febf", size = 106312 }, + { url = "https://files.pythonhosted.org/packages/67/dc/350a54bea5cf397d357534198ab8119cfd0d8e8bad623b520f9c290af985/mmh3-5.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:78ae6a03f4cff4aa92ddd690611168856f8c33a141bd3e5a1e0a85521dc21ea0", size = 104232 }, + { url = "https://files.pythonhosted.org/packages/b2/5d/2c6eb4a4ec2f7293b98a9c07cb8c64668330b46ff2b6511244339e69a7af/mmh3-5.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:95f983535b39795d9fb7336438faae117424c6798f763d67c6624f6caf2c4c01", size = 91663 }, + { url = "https://files.pythonhosted.org/packages/f1/ac/17030d24196f73ecbab8b5033591e5e0e2beca103181a843a135c78f4fee/mmh3-5.1.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d46fdd80d4c7ecadd9faa6181e92ccc6fe91c50991c9af0e371fdf8b8a7a6150", size = 99166 }, + { url = "https://files.pythonhosted.org/packages/b9/ed/54ddc56603561a10b33da9b12e95a48a271d126f4a4951841bbd13145ebf/mmh3-5.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0f16e976af7365ea3b5c425124b2a7f0147eed97fdbb36d99857f173c8d8e096", size = 101555 }, + { url = "https://files.pythonhosted.org/packages/1c/c3/33fb3a940c9b70908a5cc9fcc26534aff8698180f9f63ab6b7cc74da8bcd/mmh3-5.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:6fa97f7d1e1f74ad1565127229d510f3fd65d931fdedd707c1e15100bc9e5ebb", size = 94813 }, + { url = "https://files.pythonhosted.org/packages/61/88/c9ff76a23abe34db8eee1a6fa4e449462a16c7eb547546fc5594b0860a72/mmh3-5.1.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4052fa4a8561bd62648e9eb993c8f3af3bdedadf3d9687aa4770d10e3709a80c", size = 109611 }, + { url = "https://files.pythonhosted.org/packages/0b/8e/27d04f40e95554ebe782cac7bddda2d158cf3862387298c9c7b254fa7beb/mmh3-5.1.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:3f0e8ae9f961037f812afe3cce7da57abf734285961fffbeff9a4c011b737732", size = 100515 }, + { url = "https://files.pythonhosted.org/packages/7b/00/504ca8f462f01048f3c87cd93f2e1f60b93dac2f930cd4ed73532a9337f5/mmh3-5.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:99297f207db967814f1f02135bb7fe7628b9eacb046134a34e1015b26b06edce", size = 100177 }, + { url = "https://files.pythonhosted.org/packages/6f/1d/2efc3525fe6fdf8865972fcbb884bd1f4b0f923c19b80891cecf7e239fa5/mmh3-5.1.0-cp310-cp310-win32.whl", hash = "sha256:2e6c8dc3631a5e22007fbdb55e993b2dbce7985c14b25b572dd78403c2e79182", size = 40815 }, + { url = "https://files.pythonhosted.org/packages/38/b5/c8fbe707cb0fea77a6d2d58d497bc9b67aff80deb84d20feb34d8fdd8671/mmh3-5.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:e4e8c7ad5a4dddcfde35fd28ef96744c1ee0f9d9570108aa5f7e77cf9cfdf0bf", size = 41479 }, + { url = "https://files.pythonhosted.org/packages/a1/f1/663e16134f913fccfbcea5b300fb7dc1860d8f63dc71867b013eebc10aec/mmh3-5.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:45da549269883208912868a07d0364e1418d8292c4259ca11699ba1b2475bd26", size = 38883 }, + { url = "https://files.pythonhosted.org/packages/56/09/fda7af7fe65928262098382e3bf55950cfbf67d30bf9e47731bf862161e9/mmh3-5.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0b529dcda3f951ff363a51d5866bc6d63cf57f1e73e8961f864ae5010647079d", size = 56098 }, + { url = "https://files.pythonhosted.org/packages/0c/ab/84c7bc3f366d6f3bd8b5d9325a10c367685bc17c26dac4c068e2001a4671/mmh3-5.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4db1079b3ace965e562cdfc95847312f9273eb2ad3ebea983435c8423e06acd7", size = 40513 }, + { url = "https://files.pythonhosted.org/packages/4f/21/25ea58ca4a652bdc83d1528bec31745cce35802381fb4fe3c097905462d2/mmh3-5.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:22d31e3a0ff89b8eb3b826d6fc8e19532998b2aa6b9143698043a1268da413e1", size = 40112 }, + { url = "https://files.pythonhosted.org/packages/bd/78/4f12f16ae074ddda6f06745254fdb50f8cf3c85b0bbf7eaca58bed84bf58/mmh3-5.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2139bfbd354cd6cb0afed51c4b504f29bcd687a3b1460b7e89498329cc28a894", size = 102632 }, + { url = "https://files.pythonhosted.org/packages/48/11/8f09dc999cf2a09b6138d8d7fc734efb7b7bfdd9adb9383380941caadff0/mmh3-5.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8c8105c6a435bc2cd6ea2ef59558ab1a2976fd4a4437026f562856d08996673a", size = 108884 }, + { url = "https://files.pythonhosted.org/packages/bd/91/e59a66538a3364176f6c3f7620eee0ab195bfe26f89a95cbcc7a1fb04b28/mmh3-5.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57730067174a7f36fcd6ce012fe359bd5510fdaa5fe067bc94ed03e65dafb769", size = 106835 }, + { url = "https://files.pythonhosted.org/packages/25/14/b85836e21ab90e5cddb85fe79c494ebd8f81d96a87a664c488cc9277668b/mmh3-5.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bde80eb196d7fdc765a318604ded74a4378f02c5b46c17aa48a27d742edaded2", size = 93688 }, + { url = "https://files.pythonhosted.org/packages/ac/aa/8bc964067df9262740c95e4cde2d19f149f2224f426654e14199a9e47df6/mmh3-5.1.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9c8eddcb441abddeb419c16c56fd74b3e2df9e57f7aa2903221996718435c7a", size = 101569 }, + { url = "https://files.pythonhosted.org/packages/70/b6/1fb163cbf919046a64717466c00edabebece3f95c013853fec76dbf2df92/mmh3-5.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:99e07e4acafbccc7a28c076a847fb060ffc1406036bc2005acb1b2af620e53c3", size = 98483 }, + { url = "https://files.pythonhosted.org/packages/70/49/ba64c050dd646060f835f1db6b2cd60a6485f3b0ea04976e7a29ace7312e/mmh3-5.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9e25ba5b530e9a7d65f41a08d48f4b3fedc1e89c26486361166a5544aa4cad33", size = 96496 }, + { url = "https://files.pythonhosted.org/packages/9e/07/f2751d6a0b535bb865e1066e9c6b80852571ef8d61bce7eb44c18720fbfc/mmh3-5.1.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:bb9bf7475b4d99156ce2f0cf277c061a17560c8c10199c910a680869a278ddc7", size = 105109 }, + { url = "https://files.pythonhosted.org/packages/b7/02/30360a5a66f7abba44596d747cc1e6fb53136b168eaa335f63454ab7bb79/mmh3-5.1.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2a1b0878dd281ea3003368ab53ff6f568e175f1b39f281df1da319e58a19c23a", size = 98231 }, + { url = "https://files.pythonhosted.org/packages/8c/60/8526b0c750ff4d7ae1266e68b795f14b97758a1d9fcc19f6ecabf9c55656/mmh3-5.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:25f565093ac8b8aefe0f61f8f95c9a9d11dd69e6a9e9832ff0d293511bc36258", size = 97548 }, + { url = "https://files.pythonhosted.org/packages/6d/4c/26e1222aca65769280d5427a1ce5875ef4213449718c8f03958d0bf91070/mmh3-5.1.0-cp311-cp311-win32.whl", hash = "sha256:1e3554d8792387eac73c99c6eaea0b3f884e7130eb67986e11c403e4f9b6d372", size = 40810 }, + { url = "https://files.pythonhosted.org/packages/98/d5/424ba95062d1212ea615dc8debc8d57983f2242d5e6b82e458b89a117a1e/mmh3-5.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:8ad777a48197882492af50bf3098085424993ce850bdda406a358b6ab74be759", size = 41476 }, + { url = "https://files.pythonhosted.org/packages/bd/08/0315ccaf087ba55bb19a6dd3b1e8acd491e74ce7f5f9c4aaa06a90d66441/mmh3-5.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:f29dc4efd99bdd29fe85ed6c81915b17b2ef2cf853abf7213a48ac6fb3eaabe1", size = 38880 }, + { url = "https://files.pythonhosted.org/packages/f4/47/e5f452bdf16028bfd2edb4e2e35d0441e4a4740f30e68ccd4cfd2fb2c57e/mmh3-5.1.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:45712987367cb9235026e3cbf4334670522a97751abfd00b5bc8bfa022c3311d", size = 56152 }, + { url = "https://files.pythonhosted.org/packages/60/38/2132d537dc7a7fdd8d2e98df90186c7fcdbd3f14f95502a24ba443c92245/mmh3-5.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b1020735eb35086ab24affbea59bb9082f7f6a0ad517cb89f0fc14f16cea4dae", size = 40564 }, + { url = "https://files.pythonhosted.org/packages/c0/2a/c52cf000581bfb8d94794f58865658e7accf2fa2e90789269d4ae9560b16/mmh3-5.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:babf2a78ce5513d120c358722a2e3aa7762d6071cd10cede026f8b32452be322", size = 40104 }, + { url = "https://files.pythonhosted.org/packages/83/33/30d163ce538c54fc98258db5621447e3ab208d133cece5d2577cf913e708/mmh3-5.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4f47f58cd5cbef968c84a7c1ddc192fef0a36b48b0b8a3cb67354531aa33b00", size = 102634 }, + { url = "https://files.pythonhosted.org/packages/94/5c/5a18acb6ecc6852be2d215c3d811aa61d7e425ab6596be940877355d7f3e/mmh3-5.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2044a601c113c981f2c1e14fa33adc9b826c9017034fe193e9eb49a6882dbb06", size = 108888 }, + { url = "https://files.pythonhosted.org/packages/1f/f6/11c556324c64a92aa12f28e221a727b6e082e426dc502e81f77056f6fc98/mmh3-5.1.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c94d999c9f2eb2da44d7c2826d3fbffdbbbbcde8488d353fee7c848ecc42b968", size = 106968 }, + { url = "https://files.pythonhosted.org/packages/5d/61/ca0c196a685aba7808a5c00246f17b988a9c4f55c594ee0a02c273e404f3/mmh3-5.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a015dcb24fa0c7a78f88e9419ac74f5001c1ed6a92e70fd1803f74afb26a4c83", size = 93771 }, + { url = "https://files.pythonhosted.org/packages/b4/55/0927c33528710085ee77b808d85bbbafdb91a1db7c8eaa89cac16d6c513e/mmh3-5.1.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:457da019c491a2d20e2022c7d4ce723675e4c081d9efc3b4d8b9f28a5ea789bd", size = 101726 }, + { url = "https://files.pythonhosted.org/packages/49/39/a92c60329fa470f41c18614a93c6cd88821412a12ee78c71c3f77e1cfc2d/mmh3-5.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:71408579a570193a4ac9c77344d68ddefa440b00468a0b566dcc2ba282a9c559", size = 98523 }, + { url = "https://files.pythonhosted.org/packages/81/90/26adb15345af8d9cf433ae1b6adcf12e0a4cad1e692de4fa9f8e8536c5ae/mmh3-5.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8b3a04bc214a6e16c81f02f855e285c6df274a2084787eeafaa45f2fbdef1b63", size = 96628 }, + { url = "https://files.pythonhosted.org/packages/8a/4d/340d1e340df972a13fd4ec84c787367f425371720a1044220869c82364e9/mmh3-5.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:832dae26a35514f6d3c1e267fa48e8de3c7b978afdafa0529c808ad72e13ada3", size = 105190 }, + { url = "https://files.pythonhosted.org/packages/d3/7c/65047d1cccd3782d809936db446430fc7758bda9def5b0979887e08302a2/mmh3-5.1.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bf658a61fc92ef8a48945ebb1076ef4ad74269e353fffcb642dfa0890b13673b", size = 98439 }, + { url = "https://files.pythonhosted.org/packages/72/d2/3c259d43097c30f062050f7e861075099404e8886b5d4dd3cebf180d6e02/mmh3-5.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3313577453582b03383731b66447cdcdd28a68f78df28f10d275d7d19010c1df", size = 97780 }, + { url = "https://files.pythonhosted.org/packages/29/29/831ea8d4abe96cdb3e28b79eab49cac7f04f9c6b6e36bfc686197ddba09d/mmh3-5.1.0-cp312-cp312-win32.whl", hash = "sha256:1d6508504c531ab86c4424b5a5ff07c1132d063863339cf92f6657ff7a580f76", size = 40835 }, + { url = "https://files.pythonhosted.org/packages/12/dd/7cbc30153b73f08eeac43804c1dbc770538a01979b4094edbe1a4b8eb551/mmh3-5.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:aa75981fcdf3f21759d94f2c81b6a6e04a49dfbcdad88b152ba49b8e20544776", size = 41509 }, + { url = "https://files.pythonhosted.org/packages/80/9d/627375bab4c90dd066093fc2c9a26b86f87e26d980dbf71667b44cbee3eb/mmh3-5.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:a4c1a76808dfea47f7407a0b07aaff9087447ef6280716fd0783409b3088bb3c", size = 38888 }, + { url = "https://files.pythonhosted.org/packages/05/06/a098a42870db16c0a54a82c56a5bdc873de3165218cd5b3ca59dbc0d31a7/mmh3-5.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7a523899ca29cfb8a5239618474a435f3d892b22004b91779fcb83504c0d5b8c", size = 56165 }, + { url = "https://files.pythonhosted.org/packages/5a/65/eaada79a67fde1f43e1156d9630e2fb70655e1d3f4e8f33d7ffa31eeacfd/mmh3-5.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:17cef2c3a6ca2391ca7171a35ed574b5dab8398163129a3e3a4c05ab85a4ff40", size = 40569 }, + { url = "https://files.pythonhosted.org/packages/36/7e/2b6c43ed48be583acd68e34d16f19209a9f210e4669421b0321e326d8554/mmh3-5.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:52e12895b30110f3d89dae59a888683cc886ed0472dd2eca77497edef6161997", size = 40104 }, + { url = "https://files.pythonhosted.org/packages/11/2b/1f9e962fdde8e41b0f43d22c8ba719588de8952f9376df7d73a434827590/mmh3-5.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e0d6719045cda75c3f40397fc24ab67b18e0cb8f69d3429ab4c39763c4c608dd", size = 102497 }, + { url = "https://files.pythonhosted.org/packages/46/94/d6c5c3465387ba077cccdc028ab3eec0d86eed1eebe60dcf4d15294056be/mmh3-5.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d19fa07d303a91f8858982c37e6939834cb11893cb3ff20e6ee6fa2a7563826a", size = 108834 }, + { url = "https://files.pythonhosted.org/packages/34/1e/92c212bb81796b69dddfd50a8a8f4b26ab0d38fdaf1d3e8628a67850543b/mmh3-5.1.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:31b47a620d622fbde8ca1ca0435c5d25de0ac57ab507209245e918128e38e676", size = 106936 }, + { url = "https://files.pythonhosted.org/packages/f4/41/f2f494bbff3aad5ffd2085506255049de76cde51ddac84058e32768acc79/mmh3-5.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:00f810647c22c179b6821079f7aa306d51953ac893587ee09cf1afb35adf87cb", size = 93709 }, + { url = "https://files.pythonhosted.org/packages/9e/a9/a2cc4a756d73d9edf4fb85c76e16fd56b0300f8120fd760c76b28f457730/mmh3-5.1.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6128b610b577eed1e89ac7177ab0c33d06ade2aba93f5c89306032306b5f1c6", size = 101623 }, + { url = "https://files.pythonhosted.org/packages/5e/6f/b9d735533b6a56b2d56333ff89be6a55ac08ba7ff33465feb131992e33eb/mmh3-5.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1e550a45d2ff87a1c11b42015107f1778c93f4c6f8e731bf1b8fa770321b8cc4", size = 98521 }, + { url = "https://files.pythonhosted.org/packages/99/47/dff2b54fac0d421c1e6ecbd2d9c85b2d0e6f6ee0d10b115d9364116a511e/mmh3-5.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:785ae09276342f79fd8092633e2d52c0f7c44d56e8cfda8274ccc9b76612dba2", size = 96696 }, + { url = "https://files.pythonhosted.org/packages/be/43/9e205310f47c43ddf1575bb3a1769c36688f30f1ac105e0f0c878a29d2cd/mmh3-5.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:0f4be3703a867ef976434afd3661a33884abe73ceb4ee436cac49d3b4c2aaa7b", size = 105234 }, + { url = "https://files.pythonhosted.org/packages/6b/44/90b11fd2b67dcb513f5bfe9b476eb6ca2d5a221c79b49884dc859100905e/mmh3-5.1.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:e513983830c4ff1f205ab97152a0050cf7164f1b4783d702256d39c637b9d107", size = 98449 }, + { url = "https://files.pythonhosted.org/packages/f0/d0/25c4b0c7b8e49836541059b28e034a4cccd0936202800d43a1cc48495ecb/mmh3-5.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b9135c300535c828c0bae311b659f33a31c941572eae278568d1a953c4a57b59", size = 97796 }, + { url = "https://files.pythonhosted.org/packages/23/fa/cbbb7fcd0e287a715f1cd28a10de94c0535bd94164e38b852abc18da28c6/mmh3-5.1.0-cp313-cp313-win32.whl", hash = "sha256:c65dbd12885a5598b70140d24de5839551af5a99b29f9804bb2484b29ef07692", size = 40828 }, + { url = "https://files.pythonhosted.org/packages/09/33/9fb90ef822f7b734955a63851907cf72f8a3f9d8eb3c5706bfa6772a2a77/mmh3-5.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:10db7765201fc65003fa998faa067417ef6283eb5f9bba8f323c48fd9c33e91f", size = 41504 }, + { url = "https://files.pythonhosted.org/packages/16/71/4ad9a42f2772793a03cb698f0fc42499f04e6e8d2560ba2f7da0fb059a8e/mmh3-5.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:b22fe2e54be81f6c07dcb36b96fa250fb72effe08aa52fbb83eade6e1e2d5fd7", size = 38890 }, + { url = "https://files.pythonhosted.org/packages/44/e8/65dae27ee37a8b00bb580cebe62e79e0901f7e06210fd5cf900da1751a50/mmh3-5.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:166b67749a1d8c93b06f5e90576f1ba838a65c8e79f28ffd9dfafba7c7d0a084", size = 56106 }, + { url = "https://files.pythonhosted.org/packages/73/03/f6c27e317c52f8f12faa2f9fd237f27f50fc7bcd9b862d3d7101932319a5/mmh3-5.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:adba83c7ba5cc8ea201ee1e235f8413a68e7f7b8a657d582cc6c6c9d73f2830e", size = 40515 }, + { url = "https://files.pythonhosted.org/packages/4c/4d/83927efc3ff223c564d6b2686880ef5087b6215bbfc1b73fcb7a1e0d12ed/mmh3-5.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a61f434736106804eb0b1612d503c4e6eb22ba31b16e6a2f987473de4226fa55", size = 40108 }, + { url = "https://files.pythonhosted.org/packages/47/c6/f0e33468cc00729cb4019176288ed1506632dce12ee7a842f1c78d1f2c93/mmh3-5.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba9ce59816b30866093f048b3312c2204ff59806d3a02adee71ff7bd22b87554", size = 99892 }, + { url = "https://files.pythonhosted.org/packages/22/16/73e25fc16b17acc0de604a28c716407728777c4d52dae72a0e505e32d281/mmh3-5.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cd51597bef1e503363b05cb579db09269e6e6c39d419486626b255048daf545b", size = 106054 }, + { url = "https://files.pythonhosted.org/packages/4f/f3/f9c5e4051e8ab54fd92717906f998f7161bfd3440bcea1b6f84e956e168a/mmh3-5.1.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d51a1ed642d3fb37b8f4cab966811c52eb246c3e1740985f701ef5ad4cdd2145", size = 104004 }, + { url = "https://files.pythonhosted.org/packages/e1/18/6452be7e21f792f69e83854bc71fa4443816a6f8e909374a547f2eccdd44/mmh3-5.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:709bfe81c53bf8a3609efcbd65c72305ade60944f66138f697eefc1a86b6e356", size = 91420 }, + { url = "https://files.pythonhosted.org/packages/66/59/7e16f10ee38f56bb0890d4da2c5bf1bfd77b15ea8e91244eeebdcfc84077/mmh3-5.1.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e01a9b0092b6f82e861137c8e9bb9899375125b24012eb5219e61708be320032", size = 98910 }, + { url = "https://files.pythonhosted.org/packages/cb/78/f247daea100fb9f4a7aad3fb01aa6e258de12fc594f37415c8dc22d8bd71/mmh3-5.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:27e46a2c13c9a805e03c9ec7de0ca8e096794688ab2125bdce4229daf60c4a56", size = 101325 }, + { url = "https://files.pythonhosted.org/packages/6e/d6/07ac481d91dc4659c9c556a326e4349d08331b5713a1ac11bf7b063c6bdc/mmh3-5.1.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:5766299c1d26f6bfd0a638e070bd17dbd98d4ccb067d64db3745bf178e700ef0", size = 94625 }, + { url = "https://files.pythonhosted.org/packages/ff/3a/118c058b05f2396e3c4077183ad8f5d0e0a508c28eaae57b08d6c49a0754/mmh3-5.1.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:7785205e3e4443fdcbb73766798c7647f94c2f538b90f666688f3e757546069e", size = 109398 }, + { url = "https://files.pythonhosted.org/packages/8d/d5/accc9372f70e15db8e8e203940b25ac409e061460cd6f96aab6066bb66a6/mmh3-5.1.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:8e574fbd39afb433b3ab95683b1b4bf18313dc46456fc9daaddc2693c19ca565", size = 100319 }, + { url = "https://files.pythonhosted.org/packages/bb/98/e157770077a19322483a1ab661fce14b6489bca8da49bd6fece98918c845/mmh3-5.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1b6727a5a20e32cbf605743749f3862abe5f5e097cbf2afc7be5aafd32a549ae", size = 99946 }, + { url = "https://files.pythonhosted.org/packages/6d/65/9b5b506e8a88386316d9cff0c4b155262200d1ec95cad6d4287ec240a7fd/mmh3-5.1.0-cp39-cp39-win32.whl", hash = "sha256:d6eaa711d4b9220fe5252032a44bf68e5dcfb7b21745a96efc9e769b0dd57ec2", size = 40823 }, + { url = "https://files.pythonhosted.org/packages/bd/67/c4468b21d9d219e0d64708c07936666968c88d92e3542cfb6c47ce06768b/mmh3-5.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:49d444913f6c02980e5241a53fe9af2338f2043d6ce5b6f5ea7d302c52c604ac", size = 41481 }, + { url = "https://files.pythonhosted.org/packages/20/bb/cb97418e487632eb1f6fb0f2fa86adbeec102cbf6bfa4ebfc10a8889da2c/mmh3-5.1.0-cp39-cp39-win_arm64.whl", hash = "sha256:0daaeaedd78773b70378f2413c7d6b10239a75d955d30d54f460fb25d599942d", size = 38870 }, +] + +[[package]] +name = "multiprocess" +version = "0.70.18" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dill" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/fd/2ae3826f5be24c6ed87266bc4e59c46ea5b059a103f3d7e7eb76a52aeecb/multiprocess-0.70.18.tar.gz", hash = "sha256:f9597128e6b3e67b23956da07cf3d2e5cba79e2f4e0fba8d7903636663ec6d0d", size = 1798503 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/f8/7f9a8f08bf98cea1dfaa181e05cc8bbcb59cecf044b5a9ac3cce39f9c449/multiprocess-0.70.18-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:25d4012dcaaf66b9e8e955f58482b42910c2ee526d532844d8bcf661bbc604df", size = 135083 }, + { url = "https://files.pythonhosted.org/packages/e5/03/b7b10dbfc17b2b3ce07d4d30b3ba8367d0ed32d6d46cd166e298f161dd46/multiprocess-0.70.18-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:06b19433de0d02afe5869aec8931dd5c01d99074664f806c73896b0d9e527213", size = 135128 }, + { url = "https://files.pythonhosted.org/packages/c1/a3/5f8d3b9690ea5580bee5868ab7d7e2cfca74b7e826b28192b40aa3881cdc/multiprocess-0.70.18-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:6fa1366f994373aaf2d4738b0f56e707caeaa05486e97a7f71ee0853823180c2", size = 135132 }, + { url = "https://files.pythonhosted.org/packages/55/4d/9af0d1279c84618bcd35bf5fd7e371657358c7b0a523e54a9cffb87461f8/multiprocess-0.70.18-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:8b8940ae30139e04b076da6c5b83e9398585ebdf0f2ad3250673fef5b2ff06d6", size = 144695 }, + { url = "https://files.pythonhosted.org/packages/17/bf/87323e79dd0562474fad3373c21c66bc6c3c9963b68eb2a209deb4c8575e/multiprocess-0.70.18-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:0929ba95831adb938edbd5fb801ac45e705ecad9d100b3e653946b7716cb6bd3", size = 144742 }, + { url = "https://files.pythonhosted.org/packages/dd/74/cb8c831e58dc6d5cf450b17c7db87f14294a1df52eb391da948b5e0a0b94/multiprocess-0.70.18-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:4d77f8e4bfe6c6e2e661925bbf9aed4d5ade9a1c6502d5dfc10129b9d1141797", size = 144745 }, + { url = "https://files.pythonhosted.org/packages/12/89/733ebfc487a4381590d863222f3f0c8bac105d032102d7a96d6566823d67/multiprocess-0.70.18-pp39-pypy39_pp73-macosx_10_13_arm64.whl", hash = "sha256:9fd8d662f7524a95a1be7cbea271f0b33089fe792baabec17d93103d368907da", size = 133534 }, + { url = "https://files.pythonhosted.org/packages/41/65/d74a7955593c2c56c79d01ef07be5aa103342441673657dc7387855703de/multiprocess-0.70.18-pp39-pypy39_pp73-macosx_10_13_x86_64.whl", hash = "sha256:3fbba48bfcd932747c33f0b152b26207c4e0840c35cab359afaff7a8672b1031", size = 133533 }, + { url = "https://files.pythonhosted.org/packages/7d/5a/6f3ebcbc1508aa651cbe8deeca612b7915b97303410c93e9a4d83ba07e03/multiprocess-0.70.18-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:5f9be0342e597dde86152c10442c5fb6c07994b1c29de441b7a3a08b0e6be2a0", size = 133537 }, + { url = "https://files.pythonhosted.org/packages/ba/d8/0cba6cf51a1a31f20471fbc823a716170c73012ddc4fb85d706630ed6e8f/multiprocess-0.70.18-py310-none-any.whl", hash = "sha256:60c194974c31784019c1f459d984e8f33ee48f10fcf42c309ba97b30d9bd53ea", size = 134948 }, + { url = "https://files.pythonhosted.org/packages/4b/88/9039f2fed1012ef584751d4ceff9ab4a51e5ae264898f0b7cbf44340a859/multiprocess-0.70.18-py311-none-any.whl", hash = "sha256:5aa6eef98e691281b3ad923be2832bf1c55dd2c859acd73e5ec53a66aae06a1d", size = 144462 }, + { url = "https://files.pythonhosted.org/packages/bf/b6/5f922792be93b82ec6b5f270bbb1ef031fd0622847070bbcf9da816502cc/multiprocess-0.70.18-py312-none-any.whl", hash = "sha256:9b78f8e5024b573730bfb654783a13800c2c0f2dfc0c25e70b40d184d64adaa2", size = 150287 }, + { url = "https://files.pythonhosted.org/packages/ee/25/7d7e78e750bc1aecfaf0efbf826c69a791d2eeaf29cf20cba93ff4cced78/multiprocess-0.70.18-py313-none-any.whl", hash = "sha256:871743755f43ef57d7910a38433cfe41319e72be1bbd90b79c7a5ac523eb9334", size = 151917 }, + { url = "https://files.pythonhosted.org/packages/3b/c3/ca84c19bd14cdfc21c388fdcebf08b86a7a470ebc9f5c3c084fc2dbc50f7/multiprocess-0.70.18-py38-none-any.whl", hash = "sha256:dbf705e52a154fe5e90fb17b38f02556169557c2dd8bb084f2e06c2784d8279b", size = 132636 }, + { url = "https://files.pythonhosted.org/packages/6c/28/dd72947e59a6a8c856448a5e74da6201cb5502ddff644fbc790e4bd40b9a/multiprocess-0.70.18-py39-none-any.whl", hash = "sha256:e78ca805a72b1b810c690b6b4cc32579eba34f403094bbbae962b7b5bf9dfcb8", size = 133478 }, +] + +[[package]] +name = "mypy" +version = "1.16.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/81/69/92c7fa98112e4d9eb075a239caa4ef4649ad7d441545ccffbd5e34607cbb/mypy-1.16.1.tar.gz", hash = "sha256:6bd00a0a2094841c5e47e7374bb42b83d64c527a502e3334e1173a0c24437bab", size = 3324747 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/12/2bf23a80fcef5edb75de9a1e295d778e0f46ea89eb8b115818b663eff42b/mypy-1.16.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b4f0fed1022a63c6fec38f28b7fc77fca47fd490445c69d0a66266c59dd0b88a", size = 10958644 }, + { url = "https://files.pythonhosted.org/packages/08/50/bfe47b3b278eacf348291742fd5e6613bbc4b3434b72ce9361896417cfe5/mypy-1.16.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:86042bbf9f5a05ea000d3203cf87aa9d0ccf9a01f73f71c58979eb9249f46d72", size = 10087033 }, + { url = "https://files.pythonhosted.org/packages/21/de/40307c12fe25675a0776aaa2cdd2879cf30d99eec91b898de00228dc3ab5/mypy-1.16.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ea7469ee5902c95542bea7ee545f7006508c65c8c54b06dc2c92676ce526f3ea", size = 11875645 }, + { url = "https://files.pythonhosted.org/packages/a6/d8/85bdb59e4a98b7a31495bd8f1a4445d8ffc86cde4ab1f8c11d247c11aedc/mypy-1.16.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:352025753ef6a83cb9e7f2427319bb7875d1fdda8439d1e23de12ab164179574", size = 12616986 }, + { url = "https://files.pythonhosted.org/packages/0e/d0/bb25731158fa8f8ee9e068d3e94fcceb4971fedf1424248496292512afe9/mypy-1.16.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ff9fa5b16e4c1364eb89a4d16bcda9987f05d39604e1e6c35378a2987c1aac2d", size = 12878632 }, + { url = "https://files.pythonhosted.org/packages/2d/11/822a9beb7a2b825c0cb06132ca0a5183f8327a5e23ef89717c9474ba0bc6/mypy-1.16.1-cp310-cp310-win_amd64.whl", hash = "sha256:1256688e284632382f8f3b9e2123df7d279f603c561f099758e66dd6ed4e8bd6", size = 9484391 }, + { url = "https://files.pythonhosted.org/packages/9a/61/ec1245aa1c325cb7a6c0f8570a2eee3bfc40fa90d19b1267f8e50b5c8645/mypy-1.16.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:472e4e4c100062488ec643f6162dd0d5208e33e2f34544e1fc931372e806c0cc", size = 10890557 }, + { url = "https://files.pythonhosted.org/packages/6b/bb/6eccc0ba0aa0c7a87df24e73f0ad34170514abd8162eb0c75fd7128171fb/mypy-1.16.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ea16e2a7d2714277e349e24d19a782a663a34ed60864006e8585db08f8ad1782", size = 10012921 }, + { url = "https://files.pythonhosted.org/packages/5f/80/b337a12e2006715f99f529e732c5f6a8c143bb58c92bb142d5ab380963a5/mypy-1.16.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:08e850ea22adc4d8a4014651575567b0318ede51e8e9fe7a68f25391af699507", size = 11802887 }, + { url = "https://files.pythonhosted.org/packages/d9/59/f7af072d09793d581a745a25737c7c0a945760036b16aeb620f658a017af/mypy-1.16.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22d76a63a42619bfb90122889b903519149879ddbf2ba4251834727944c8baca", size = 12531658 }, + { url = "https://files.pythonhosted.org/packages/82/c4/607672f2d6c0254b94a646cfc45ad589dd71b04aa1f3d642b840f7cce06c/mypy-1.16.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2c7ce0662b6b9dc8f4ed86eb7a5d505ee3298c04b40ec13b30e572c0e5ae17c4", size = 12732486 }, + { url = "https://files.pythonhosted.org/packages/b6/5e/136555ec1d80df877a707cebf9081bd3a9f397dedc1ab9750518d87489ec/mypy-1.16.1-cp311-cp311-win_amd64.whl", hash = "sha256:211287e98e05352a2e1d4e8759c5490925a7c784ddc84207f4714822f8cf99b6", size = 9479482 }, + { url = "https://files.pythonhosted.org/packages/b4/d6/39482e5fcc724c15bf6280ff5806548c7185e0c090712a3736ed4d07e8b7/mypy-1.16.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:af4792433f09575d9eeca5c63d7d90ca4aeceda9d8355e136f80f8967639183d", size = 11066493 }, + { url = "https://files.pythonhosted.org/packages/e6/e5/26c347890efc6b757f4d5bb83f4a0cf5958b8cf49c938ac99b8b72b420a6/mypy-1.16.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:66df38405fd8466ce3517eda1f6640611a0b8e70895e2a9462d1d4323c5eb4b9", size = 10081687 }, + { url = "https://files.pythonhosted.org/packages/44/c7/b5cb264c97b86914487d6a24bd8688c0172e37ec0f43e93b9691cae9468b/mypy-1.16.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:44e7acddb3c48bd2713994d098729494117803616e116032af192871aed80b79", size = 11839723 }, + { url = "https://files.pythonhosted.org/packages/15/f8/491997a9b8a554204f834ed4816bda813aefda31cf873bb099deee3c9a99/mypy-1.16.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0ab5eca37b50188163fa7c1b73c685ac66c4e9bdee4a85c9adac0e91d8895e15", size = 12722980 }, + { url = "https://files.pythonhosted.org/packages/df/f0/2bd41e174b5fd93bc9de9a28e4fb673113633b8a7f3a607fa4a73595e468/mypy-1.16.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb6229b2c9086247e21a83c309754b9058b438704ad2f6807f0d8227f6ebdd", size = 12903328 }, + { url = "https://files.pythonhosted.org/packages/61/81/5572108a7bec2c46b8aff7e9b524f371fe6ab5efb534d38d6b37b5490da8/mypy-1.16.1-cp312-cp312-win_amd64.whl", hash = "sha256:1f0435cf920e287ff68af3d10a118a73f212deb2ce087619eb4e648116d1fe9b", size = 9562321 }, + { url = "https://files.pythonhosted.org/packages/28/e3/96964af4a75a949e67df4b95318fe2b7427ac8189bbc3ef28f92a1c5bc56/mypy-1.16.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ddc91eb318c8751c69ddb200a5937f1232ee8efb4e64e9f4bc475a33719de438", size = 11063480 }, + { url = "https://files.pythonhosted.org/packages/f5/4d/cd1a42b8e5be278fab7010fb289d9307a63e07153f0ae1510a3d7b703193/mypy-1.16.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:87ff2c13d58bdc4bbe7dc0dedfe622c0f04e2cb2a492269f3b418df2de05c536", size = 10090538 }, + { url = "https://files.pythonhosted.org/packages/c9/4f/c3c6b4b66374b5f68bab07c8cabd63a049ff69796b844bc759a0ca99bb2a/mypy-1.16.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a7cfb0fe29fe5a9841b7c8ee6dffb52382c45acdf68f032145b75620acfbd6f", size = 11836839 }, + { url = "https://files.pythonhosted.org/packages/b4/7e/81ca3b074021ad9775e5cb97ebe0089c0f13684b066a750b7dc208438403/mypy-1.16.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:051e1677689c9d9578b9c7f4d206d763f9bbd95723cd1416fad50db49d52f359", size = 12715634 }, + { url = "https://files.pythonhosted.org/packages/e9/95/bdd40c8be346fa4c70edb4081d727a54d0a05382d84966869738cfa8a497/mypy-1.16.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d5d2309511cc56c021b4b4e462907c2b12f669b2dbeb68300110ec27723971be", size = 12895584 }, + { url = "https://files.pythonhosted.org/packages/5a/fd/d486a0827a1c597b3b48b1bdef47228a6e9ee8102ab8c28f944cb83b65dc/mypy-1.16.1-cp313-cp313-win_amd64.whl", hash = "sha256:4f58ac32771341e38a853c5d0ec0dfe27e18e27da9cdb8bbc882d2249c71a3ee", size = 9573886 }, + { url = "https://files.pythonhosted.org/packages/49/5e/ed1e6a7344005df11dfd58b0fdd59ce939a0ba9f7ed37754bf20670b74db/mypy-1.16.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7fc688329af6a287567f45cc1cefb9db662defeb14625213a5b7da6e692e2069", size = 10959511 }, + { url = "https://files.pythonhosted.org/packages/30/88/a7cbc2541e91fe04f43d9e4577264b260fecedb9bccb64ffb1a34b7e6c22/mypy-1.16.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5e198ab3f55924c03ead626ff424cad1732d0d391478dfbf7bb97b34602395da", size = 10075555 }, + { url = "https://files.pythonhosted.org/packages/93/f7/c62b1e31a32fbd1546cca5e0a2e5f181be5761265ad1f2e94f2a306fa906/mypy-1.16.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09aa4f91ada245f0a45dbc47e548fd94e0dd5a8433e0114917dc3b526912a30c", size = 11874169 }, + { url = "https://files.pythonhosted.org/packages/c8/15/db580a28034657fb6cb87af2f8996435a5b19d429ea4dcd6e1c73d418e60/mypy-1.16.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13c7cd5b1cb2909aa318a90fd1b7e31f17c50b242953e7dd58345b2a814f6383", size = 12610060 }, + { url = "https://files.pythonhosted.org/packages/ec/78/c17f48f6843048fa92d1489d3095e99324f2a8c420f831a04ccc454e2e51/mypy-1.16.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:58e07fb958bc5d752a280da0e890c538f1515b79a65757bbdc54252ba82e0b40", size = 12875199 }, + { url = "https://files.pythonhosted.org/packages/bc/d6/ed42167d0a42680381653fd251d877382351e1bd2c6dd8a818764be3beb1/mypy-1.16.1-cp39-cp39-win_amd64.whl", hash = "sha256:f895078594d918f93337a505f8add9bd654d1a24962b4c6ed9390e12531eb31b", size = 9487033 }, + { url = "https://files.pythonhosted.org/packages/cf/d3/53e684e78e07c1a2bf7105715e5edd09ce951fc3f47cf9ed095ec1b7a037/mypy-1.16.1-py3-none-any.whl", hash = "sha256:5fc2ac4027d0ef28d6ba69a0343737a23c4d1b83672bf38d1fe237bdc0643b37", size = 2265923 }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963 }, +] + +[[package]] +name = "mypy-protobuf" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, + { name = "types-protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4d/6f/282d64d66bf48ce60e38a6560753f784e0f88ab245ac2fb5e93f701a36cd/mypy-protobuf-3.6.0.tar.gz", hash = "sha256:02f242eb3409f66889f2b1a3aa58356ec4d909cdd0f93115622e9e70366eca3c", size = 24445 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/73/d6b999782ae22f16971cc05378b3b33f6a89ede3b9619e8366aa23484bca/mypy_protobuf-3.6.0-py3-none-any.whl", hash = "sha256:56176e4d569070e7350ea620262478b49b7efceba4103d468448f1d21492fd6c", size = 16434 }, +] + +[[package]] +name = "numba" +version = "0.60.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "llvmlite", version = "0.43.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3c/93/2849300a9184775ba274aba6f82f303343669b0592b7bb0849ea713dabb0/numba-0.60.0.tar.gz", hash = "sha256:5df6158e5584eece5fc83294b949fd30b9f1125df7708862205217e068aabf16", size = 2702171 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/cf/baa13a7e3556d73d9e38021e6d6aa4aeb30d8b94545aa8b70d0f24a1ccc4/numba-0.60.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5d761de835cd38fb400d2c26bb103a2726f548dc30368853121d66201672e651", size = 2647627 }, + { url = "https://files.pythonhosted.org/packages/ac/ba/4b57fa498564457c3cc9fc9e570a6b08e6086c74220f24baaf04e54b995f/numba-0.60.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:159e618ef213fba758837f9837fb402bbe65326e60ba0633dbe6c7f274d42c1b", size = 2650322 }, + { url = "https://files.pythonhosted.org/packages/28/98/7ea97ee75870a54f938a8c70f7e0be4495ba5349c5f9db09d467c4a5d5b7/numba-0.60.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1527dc578b95c7c4ff248792ec33d097ba6bef9eda466c948b68dfc995c25781", size = 3407390 }, + { url = "https://files.pythonhosted.org/packages/79/58/cb4ac5b8f7ec64200460aef1fed88258fb872ceef504ab1f989d2ff0f684/numba-0.60.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe0b28abb8d70f8160798f4de9d486143200f34458d34c4a214114e445d7124e", size = 3699694 }, + { url = "https://files.pythonhosted.org/packages/1c/b0/c61a93ca947d12233ff45de506ddbf52af3f752066a0b8be4d27426e16da/numba-0.60.0-cp310-cp310-win_amd64.whl", hash = "sha256:19407ced081d7e2e4b8d8c36aa57b7452e0283871c296e12d798852bc7d7f198", size = 2687030 }, + { url = "https://files.pythonhosted.org/packages/98/ad/df18d492a8f00d29a30db307904b9b296e37507034eedb523876f3a2e13e/numba-0.60.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a17b70fc9e380ee29c42717e8cc0bfaa5556c416d94f9aa96ba13acb41bdece8", size = 2647254 }, + { url = "https://files.pythonhosted.org/packages/9a/51/a4dc2c01ce7a850b8e56ff6d5381d047a5daea83d12bad08aa071d34b2ee/numba-0.60.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3fb02b344a2a80efa6f677aa5c40cd5dd452e1b35f8d1c2af0dfd9ada9978e4b", size = 2649970 }, + { url = "https://files.pythonhosted.org/packages/f9/4c/8889ac94c0b33dca80bed11564b8c6d9ea14d7f094e674c58e5c5b05859b/numba-0.60.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5f4fde652ea604ea3c86508a3fb31556a6157b2c76c8b51b1d45eb40c8598703", size = 3412492 }, + { url = "https://files.pythonhosted.org/packages/57/03/2b4245b05b71c0cee667e6a0b51606dfa7f4157c9093d71c6b208385a611/numba-0.60.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4142d7ac0210cc86432b818338a2bc368dc773a2f5cf1e32ff7c5b378bd63ee8", size = 3705018 }, + { url = "https://files.pythonhosted.org/packages/79/89/2d924ca60dbf949f18a6fec223a2445f5f428d9a5f97a6b29c2122319015/numba-0.60.0-cp311-cp311-win_amd64.whl", hash = "sha256:cac02c041e9b5bc8cf8f2034ff6f0dbafccd1ae9590dc146b3a02a45e53af4e2", size = 2686920 }, + { url = "https://files.pythonhosted.org/packages/eb/5c/b5ec752c475e78a6c3676b67c514220dbde2725896bbb0b6ec6ea54b2738/numba-0.60.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d7da4098db31182fc5ffe4bc42c6f24cd7d1cb8a14b59fd755bfee32e34b8404", size = 2647866 }, + { url = "https://files.pythonhosted.org/packages/65/42/39559664b2e7c15689a638c2a38b3b74c6e69a04e2b3019b9f7742479188/numba-0.60.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:38d6ea4c1f56417076ecf8fc327c831ae793282e0ff51080c5094cb726507b1c", size = 2650208 }, + { url = "https://files.pythonhosted.org/packages/67/88/c4459ccc05674ef02119abf2888ccd3e2fed12a323f52255f4982fc95876/numba-0.60.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:62908d29fb6a3229c242e981ca27e32a6e606cc253fc9e8faeb0e48760de241e", size = 3466946 }, + { url = "https://files.pythonhosted.org/packages/8b/41/ac11cf33524def12aa5bd698226ae196a1185831c05ed29dc0c56eaa308b/numba-0.60.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0ebaa91538e996f708f1ab30ef4d3ddc344b64b5227b67a57aa74f401bb68b9d", size = 3761463 }, + { url = "https://files.pythonhosted.org/packages/ca/bd/0fe29fcd1b6a8de479a4ed25c6e56470e467e3611c079d55869ceef2b6d1/numba-0.60.0-cp312-cp312-win_amd64.whl", hash = "sha256:f75262e8fe7fa96db1dca93d53a194a38c46da28b112b8a4aca168f0df860347", size = 2707588 }, + { url = "https://files.pythonhosted.org/packages/68/1a/87c53f836cdf557083248c3f47212271f220280ff766538795e77c8c6bbf/numba-0.60.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:01ef4cd7d83abe087d644eaa3d95831b777aa21d441a23703d649e06b8e06b74", size = 2647186 }, + { url = "https://files.pythonhosted.org/packages/28/14/a5baa1f2edea7b49afa4dc1bb1b126645198cf1075186853b5b497be826e/numba-0.60.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:819a3dfd4630d95fd574036f99e47212a1af41cbcb019bf8afac63ff56834449", size = 2650038 }, + { url = "https://files.pythonhosted.org/packages/3b/bd/f1985719ff34e37e07bb18f9d3acd17e5a21da255f550c8eae031e2ddf5f/numba-0.60.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0b983bd6ad82fe868493012487f34eae8bf7dd94654951404114f23c3466d34b", size = 3403010 }, + { url = "https://files.pythonhosted.org/packages/54/9b/cd73d3f6617ddc8398a63ef97d8dc9139a9879b9ca8a7ca4b8789056ea46/numba-0.60.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c151748cd269ddeab66334bd754817ffc0cabd9433acb0f551697e5151917d25", size = 3695086 }, + { url = "https://files.pythonhosted.org/packages/01/01/8b7b670c77c5ea0e47e283d82332969bf672ab6410d0b2610cac5b7a3ded/numba-0.60.0-cp39-cp39-win_amd64.whl", hash = "sha256:3031547a015710140e8c87226b4cfe927cac199835e5bf7d4fe5cb64e814e3ab", size = 2686978 }, +] + +[[package]] +name = "numba" +version = "0.61.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "llvmlite", version = "0.44.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1c/a0/e21f57604304aa03ebb8e098429222722ad99176a4f979d34af1d1ee80da/numba-0.61.2.tar.gz", hash = "sha256:8750ee147940a6637b80ecf7f95062185ad8726c8c28a2295b8ec1160a196f7d", size = 2820615 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/ca/f470be59552ccbf9531d2d383b67ae0b9b524d435fb4a0d229fef135116e/numba-0.61.2-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:cf9f9fc00d6eca0c23fc840817ce9f439b9f03c8f03d6246c0e7f0cb15b7162a", size = 2775663 }, + { url = "https://files.pythonhosted.org/packages/f5/13/3bdf52609c80d460a3b4acfb9fdb3817e392875c0d6270cf3fd9546f138b/numba-0.61.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ea0247617edcb5dd61f6106a56255baab031acc4257bddaeddb3a1003b4ca3fd", size = 2778344 }, + { url = "https://files.pythonhosted.org/packages/e2/7d/bfb2805bcfbd479f04f835241ecf28519f6e3609912e3a985aed45e21370/numba-0.61.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ae8c7a522c26215d5f62ebec436e3d341f7f590079245a2f1008dfd498cc1642", size = 3824054 }, + { url = "https://files.pythonhosted.org/packages/e3/27/797b2004745c92955470c73c82f0e300cf033c791f45bdecb4b33b12bdea/numba-0.61.2-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:bd1e74609855aa43661edffca37346e4e8462f6903889917e9f41db40907daa2", size = 3518531 }, + { url = "https://files.pythonhosted.org/packages/b1/c6/c2fb11e50482cb310afae87a997707f6c7d8a48967b9696271347441f650/numba-0.61.2-cp310-cp310-win_amd64.whl", hash = "sha256:ae45830b129c6137294093b269ef0a22998ccc27bf7cf096ab8dcf7bca8946f9", size = 2831612 }, + { url = "https://files.pythonhosted.org/packages/3f/97/c99d1056aed767503c228f7099dc11c402906b42a4757fec2819329abb98/numba-0.61.2-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:efd3db391df53aaa5cfbee189b6c910a5b471488749fd6606c3f33fc984c2ae2", size = 2775825 }, + { url = "https://files.pythonhosted.org/packages/95/9e/63c549f37136e892f006260c3e2613d09d5120672378191f2dc387ba65a2/numba-0.61.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:49c980e4171948ffebf6b9a2520ea81feed113c1f4890747ba7f59e74be84b1b", size = 2778695 }, + { url = "https://files.pythonhosted.org/packages/97/c8/8740616c8436c86c1b9a62e72cb891177d2c34c2d24ddcde4c390371bf4c/numba-0.61.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3945615cd73c2c7eba2a85ccc9c1730c21cd3958bfcf5a44302abae0fb07bb60", size = 3829227 }, + { url = "https://files.pythonhosted.org/packages/fc/06/66e99ae06507c31d15ff3ecd1f108f2f59e18b6e08662cd5f8a5853fbd18/numba-0.61.2-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:bbfdf4eca202cebade0b7d43896978e146f39398909a42941c9303f82f403a18", size = 3523422 }, + { url = "https://files.pythonhosted.org/packages/0f/a4/2b309a6a9f6d4d8cfba583401c7c2f9ff887adb5d54d8e2e130274c0973f/numba-0.61.2-cp311-cp311-win_amd64.whl", hash = "sha256:76bcec9f46259cedf888041b9886e257ae101c6268261b19fda8cfbc52bec9d1", size = 2831505 }, + { url = "https://files.pythonhosted.org/packages/b4/a0/c6b7b9c615cfa3b98c4c63f4316e3f6b3bbe2387740277006551784218cd/numba-0.61.2-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:34fba9406078bac7ab052efbf0d13939426c753ad72946baaa5bf9ae0ebb8dd2", size = 2776626 }, + { url = "https://files.pythonhosted.org/packages/92/4a/fe4e3c2ecad72d88f5f8cd04e7f7cff49e718398a2fac02d2947480a00ca/numba-0.61.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4ddce10009bc097b080fc96876d14c051cc0c7679e99de3e0af59014dab7dfe8", size = 2779287 }, + { url = "https://files.pythonhosted.org/packages/9a/2d/e518df036feab381c23a624dac47f8445ac55686ec7f11083655eb707da3/numba-0.61.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5b1bb509d01f23d70325d3a5a0e237cbc9544dd50e50588bc581ba860c213546", size = 3885928 }, + { url = "https://files.pythonhosted.org/packages/10/0f/23cced68ead67b75d77cfcca3df4991d1855c897ee0ff3fe25a56ed82108/numba-0.61.2-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:48a53a3de8f8793526cbe330f2a39fe9a6638efcbf11bd63f3d2f9757ae345cd", size = 3577115 }, + { url = "https://files.pythonhosted.org/packages/68/1d/ddb3e704c5a8fb90142bf9dc195c27db02a08a99f037395503bfbc1d14b3/numba-0.61.2-cp312-cp312-win_amd64.whl", hash = "sha256:97cf4f12c728cf77c9c1d7c23707e4d8fb4632b46275f8f3397de33e5877af18", size = 2831929 }, + { url = "https://files.pythonhosted.org/packages/0b/f3/0fe4c1b1f2569e8a18ad90c159298d862f96c3964392a20d74fc628aee44/numba-0.61.2-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:3a10a8fc9afac40b1eac55717cece1b8b1ac0b946f5065c89e00bde646b5b154", size = 2771785 }, + { url = "https://files.pythonhosted.org/packages/e9/71/91b277d712e46bd5059f8a5866862ed1116091a7cb03bd2704ba8ebe015f/numba-0.61.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7d3bcada3c9afba3bed413fba45845f2fb9cd0d2b27dd58a1be90257e293d140", size = 2773289 }, + { url = "https://files.pythonhosted.org/packages/0d/e0/5ea04e7ad2c39288c0f0f9e8d47638ad70f28e275d092733b5817cf243c9/numba-0.61.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bdbca73ad81fa196bd53dc12e3aaf1564ae036e0c125f237c7644fe64a4928ab", size = 3893918 }, + { url = "https://files.pythonhosted.org/packages/17/58/064f4dcb7d7e9412f16ecf80ed753f92297e39f399c905389688cf950b81/numba-0.61.2-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:5f154aaea625fb32cfbe3b80c5456d514d416fcdf79733dd69c0df3a11348e9e", size = 3584056 }, + { url = "https://files.pythonhosted.org/packages/af/a4/6d3a0f2d3989e62a18749e1e9913d5fa4910bbb3e3311a035baea6caf26d/numba-0.61.2-cp313-cp313-win_amd64.whl", hash = "sha256:59321215e2e0ac5fa928a8020ab00b8e57cda8a97384963ac0dfa4d4e6aa54e7", size = 2831846 }, +] + +[[package]] +name = "numpy" +version = "2.0.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/a9/75/10dd1f8116a8b796cb2c737b674e02d02e80454bda953fa7e65d8c12b016/numpy-2.0.2.tar.gz", hash = "sha256:883c987dee1880e2a864ab0dc9892292582510604156762362d9326444636e78", size = 18902015 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/21/91/3495b3237510f79f5d81f2508f9f13fea78ebfdf07538fc7444badda173d/numpy-2.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:51129a29dbe56f9ca83438b706e2e69a39892b5eda6cedcb6b0c9fdc9b0d3ece", size = 21165245 }, + { url = "https://files.pythonhosted.org/packages/05/33/26178c7d437a87082d11019292dce6d3fe6f0e9026b7b2309cbf3e489b1d/numpy-2.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f15975dfec0cf2239224d80e32c3170b1d168335eaedee69da84fbe9f1f9cd04", size = 13738540 }, + { url = "https://files.pythonhosted.org/packages/ec/31/cc46e13bf07644efc7a4bf68df2df5fb2a1a88d0cd0da9ddc84dc0033e51/numpy-2.0.2-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:8c5713284ce4e282544c68d1c3b2c7161d38c256d2eefc93c1d683cf47683e66", size = 5300623 }, + { url = "https://files.pythonhosted.org/packages/6e/16/7bfcebf27bb4f9d7ec67332ffebee4d1bf085c84246552d52dbb548600e7/numpy-2.0.2-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:becfae3ddd30736fe1889a37f1f580e245ba79a5855bff5f2a29cb3ccc22dd7b", size = 6901774 }, + { url = "https://files.pythonhosted.org/packages/f9/a3/561c531c0e8bf082c5bef509d00d56f82e0ea7e1e3e3a7fc8fa78742a6e5/numpy-2.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2da5960c3cf0df7eafefd806d4e612c5e19358de82cb3c343631188991566ccd", size = 13907081 }, + { url = "https://files.pythonhosted.org/packages/fa/66/f7177ab331876200ac7563a580140643d1179c8b4b6a6b0fc9838de2a9b8/numpy-2.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:496f71341824ed9f3d2fd36cf3ac57ae2e0165c143b55c3a035ee219413f3318", size = 19523451 }, + { url = "https://files.pythonhosted.org/packages/25/7f/0b209498009ad6453e4efc2c65bcdf0ae08a182b2b7877d7ab38a92dc542/numpy-2.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a61ec659f68ae254e4d237816e33171497e978140353c0c2038d46e63282d0c8", size = 19927572 }, + { url = "https://files.pythonhosted.org/packages/3e/df/2619393b1e1b565cd2d4c4403bdd979621e2c4dea1f8532754b2598ed63b/numpy-2.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d731a1c6116ba289c1e9ee714b08a8ff882944d4ad631fd411106a30f083c326", size = 14400722 }, + { url = "https://files.pythonhosted.org/packages/22/ad/77e921b9f256d5da36424ffb711ae79ca3f451ff8489eeca544d0701d74a/numpy-2.0.2-cp310-cp310-win32.whl", hash = "sha256:984d96121c9f9616cd33fbd0618b7f08e0cfc9600a7ee1d6fd9b239186d19d97", size = 6472170 }, + { url = "https://files.pythonhosted.org/packages/10/05/3442317535028bc29cf0c0dd4c191a4481e8376e9f0db6bcf29703cadae6/numpy-2.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:c7b0be4ef08607dd04da4092faee0b86607f111d5ae68036f16cc787e250a131", size = 15905558 }, + { url = "https://files.pythonhosted.org/packages/8b/cf/034500fb83041aa0286e0fb16e7c76e5c8b67c0711bb6e9e9737a717d5fe/numpy-2.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:49ca4decb342d66018b01932139c0961a8f9ddc7589611158cb3c27cbcf76448", size = 21169137 }, + { url = "https://files.pythonhosted.org/packages/4a/d9/32de45561811a4b87fbdee23b5797394e3d1504b4a7cf40c10199848893e/numpy-2.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:11a76c372d1d37437857280aa142086476136a8c0f373b2e648ab2c8f18fb195", size = 13703552 }, + { url = "https://files.pythonhosted.org/packages/c1/ca/2f384720020c7b244d22508cb7ab23d95f179fcfff33c31a6eeba8d6c512/numpy-2.0.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:807ec44583fd708a21d4a11d94aedf2f4f3c3719035c76a2bbe1fe8e217bdc57", size = 5298957 }, + { url = "https://files.pythonhosted.org/packages/0e/78/a3e4f9fb6aa4e6fdca0c5428e8ba039408514388cf62d89651aade838269/numpy-2.0.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8cafab480740e22f8d833acefed5cc87ce276f4ece12fdaa2e8903db2f82897a", size = 6905573 }, + { url = "https://files.pythonhosted.org/packages/a0/72/cfc3a1beb2caf4efc9d0b38a15fe34025230da27e1c08cc2eb9bfb1c7231/numpy-2.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a15f476a45e6e5a3a79d8a14e62161d27ad897381fecfa4a09ed5322f2085669", size = 13914330 }, + { url = "https://files.pythonhosted.org/packages/ba/a8/c17acf65a931ce551fee11b72e8de63bf7e8a6f0e21add4c937c83563538/numpy-2.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13e689d772146140a252c3a28501da66dfecd77490b498b168b501835041f951", size = 19534895 }, + { url = "https://files.pythonhosted.org/packages/ba/86/8767f3d54f6ae0165749f84648da9dcc8cd78ab65d415494962c86fac80f/numpy-2.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9ea91dfb7c3d1c56a0e55657c0afb38cf1eeae4544c208dc465c3c9f3a7c09f9", size = 19937253 }, + { url = "https://files.pythonhosted.org/packages/df/87/f76450e6e1c14e5bb1eae6836478b1028e096fd02e85c1c37674606ab752/numpy-2.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c1c9307701fec8f3f7a1e6711f9089c06e6284b3afbbcd259f7791282d660a15", size = 14414074 }, + { url = "https://files.pythonhosted.org/packages/5c/ca/0f0f328e1e59f73754f06e1adfb909de43726d4f24c6a3f8805f34f2b0fa/numpy-2.0.2-cp311-cp311-win32.whl", hash = "sha256:a392a68bd329eafac5817e5aefeb39038c48b671afd242710b451e76090e81f4", size = 6470640 }, + { url = "https://files.pythonhosted.org/packages/eb/57/3a3f14d3a759dcf9bf6e9eda905794726b758819df4663f217d658a58695/numpy-2.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:286cd40ce2b7d652a6f22efdfc6d1edf879440e53e76a75955bc0c826c7e64dc", size = 15910230 }, + { url = "https://files.pythonhosted.org/packages/45/40/2e117be60ec50d98fa08c2f8c48e09b3edea93cfcabd5a9ff6925d54b1c2/numpy-2.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:df55d490dea7934f330006d0f81e8551ba6010a5bf035a249ef61a94f21c500b", size = 20895803 }, + { url = "https://files.pythonhosted.org/packages/46/92/1b8b8dee833f53cef3e0a3f69b2374467789e0bb7399689582314df02651/numpy-2.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8df823f570d9adf0978347d1f926b2a867d5608f434a7cff7f7908c6570dcf5e", size = 13471835 }, + { url = "https://files.pythonhosted.org/packages/7f/19/e2793bde475f1edaea6945be141aef6c8b4c669b90c90a300a8954d08f0a/numpy-2.0.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9a92ae5c14811e390f3767053ff54eaee3bf84576d99a2456391401323f4ec2c", size = 5038499 }, + { url = "https://files.pythonhosted.org/packages/e3/ff/ddf6dac2ff0dd50a7327bcdba45cb0264d0e96bb44d33324853f781a8f3c/numpy-2.0.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:a842d573724391493a97a62ebbb8e731f8a5dcc5d285dfc99141ca15a3302d0c", size = 6633497 }, + { url = "https://files.pythonhosted.org/packages/72/21/67f36eac8e2d2cd652a2e69595a54128297cdcb1ff3931cfc87838874bd4/numpy-2.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c05e238064fc0610c840d1cf6a13bf63d7e391717d247f1bf0318172e759e692", size = 13621158 }, + { url = "https://files.pythonhosted.org/packages/39/68/e9f1126d757653496dbc096cb429014347a36b228f5a991dae2c6b6cfd40/numpy-2.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0123ffdaa88fa4ab64835dcbde75dcdf89c453c922f18dced6e27c90d1d0ec5a", size = 19236173 }, + { url = "https://files.pythonhosted.org/packages/d1/e9/1f5333281e4ebf483ba1c888b1d61ba7e78d7e910fdd8e6499667041cc35/numpy-2.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:96a55f64139912d61de9137f11bf39a55ec8faec288c75a54f93dfd39f7eb40c", size = 19634174 }, + { url = "https://files.pythonhosted.org/packages/71/af/a469674070c8d8408384e3012e064299f7a2de540738a8e414dcfd639996/numpy-2.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ec9852fb39354b5a45a80bdab5ac02dd02b15f44b3804e9f00c556bf24b4bded", size = 14099701 }, + { url = "https://files.pythonhosted.org/packages/d0/3d/08ea9f239d0e0e939b6ca52ad403c84a2bce1bde301a8eb4888c1c1543f1/numpy-2.0.2-cp312-cp312-win32.whl", hash = "sha256:671bec6496f83202ed2d3c8fdc486a8fc86942f2e69ff0e986140339a63bcbe5", size = 6174313 }, + { url = "https://files.pythonhosted.org/packages/b2/b5/4ac39baebf1fdb2e72585c8352c56d063b6126be9fc95bd2bb5ef5770c20/numpy-2.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:cfd41e13fdc257aa5778496b8caa5e856dc4896d4ccf01841daee1d96465467a", size = 15606179 }, + { url = "https://files.pythonhosted.org/packages/43/c1/41c8f6df3162b0c6ffd4437d729115704bd43363de0090c7f913cfbc2d89/numpy-2.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9059e10581ce4093f735ed23f3b9d283b9d517ff46009ddd485f1747eb22653c", size = 21169942 }, + { url = "https://files.pythonhosted.org/packages/39/bc/fd298f308dcd232b56a4031fd6ddf11c43f9917fbc937e53762f7b5a3bb1/numpy-2.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:423e89b23490805d2a5a96fe40ec507407b8ee786d66f7328be214f9679df6dd", size = 13711512 }, + { url = "https://files.pythonhosted.org/packages/96/ff/06d1aa3eeb1c614eda245c1ba4fb88c483bee6520d361641331872ac4b82/numpy-2.0.2-cp39-cp39-macosx_14_0_arm64.whl", hash = "sha256:2b2955fa6f11907cf7a70dab0d0755159bca87755e831e47932367fc8f2f2d0b", size = 5306976 }, + { url = "https://files.pythonhosted.org/packages/2d/98/121996dcfb10a6087a05e54453e28e58694a7db62c5a5a29cee14c6e047b/numpy-2.0.2-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:97032a27bd9d8988b9a97a8c4d2c9f2c15a81f61e2f21404d7e8ef00cb5be729", size = 6906494 }, + { url = "https://files.pythonhosted.org/packages/15/31/9dffc70da6b9bbf7968f6551967fc21156207366272c2a40b4ed6008dc9b/numpy-2.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e795a8be3ddbac43274f18588329c72939870a16cae810c2b73461c40718ab1", size = 13912596 }, + { url = "https://files.pythonhosted.org/packages/b9/14/78635daab4b07c0930c919d451b8bf8c164774e6a3413aed04a6d95758ce/numpy-2.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26b258c385842546006213344c50655ff1555a9338e2e5e02a0756dc3e803dd", size = 19526099 }, + { url = "https://files.pythonhosted.org/packages/26/4c/0eeca4614003077f68bfe7aac8b7496f04221865b3a5e7cb230c9d055afd/numpy-2.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5fec9451a7789926bcf7c2b8d187292c9f93ea30284802a0ab3f5be8ab36865d", size = 19932823 }, + { url = "https://files.pythonhosted.org/packages/f1/46/ea25b98b13dccaebddf1a803f8c748680d972e00507cd9bc6dcdb5aa2ac1/numpy-2.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9189427407d88ff25ecf8f12469d4d39d35bee1db5d39fc5c168c6f088a6956d", size = 14404424 }, + { url = "https://files.pythonhosted.org/packages/c8/a6/177dd88d95ecf07e722d21008b1b40e681a929eb9e329684d449c36586b2/numpy-2.0.2-cp39-cp39-win32.whl", hash = "sha256:905d16e0c60200656500c95b6b8dca5d109e23cb24abc701d41c02d74c6b3afa", size = 6476809 }, + { url = "https://files.pythonhosted.org/packages/ea/2b/7fc9f4e7ae5b507c1a3a21f0f15ed03e794c1242ea8a242ac158beb56034/numpy-2.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:a3f4ab0caa7f053f6797fcd4e1e25caee367db3112ef2b6ef82d749530768c73", size = 15911314 }, + { url = "https://files.pythonhosted.org/packages/8f/3b/df5a870ac6a3be3a86856ce195ef42eec7ae50d2a202be1f5a4b3b340e14/numpy-2.0.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7f0a0c6f12e07fa94133c8a67404322845220c06a9e80e85999afe727f7438b8", size = 21025288 }, + { url = "https://files.pythonhosted.org/packages/2c/97/51af92f18d6f6f2d9ad8b482a99fb74e142d71372da5d834b3a2747a446e/numpy-2.0.2-pp39-pypy39_pp73-macosx_14_0_x86_64.whl", hash = "sha256:312950fdd060354350ed123c0e25a71327d3711584beaef30cdaa93320c392d4", size = 6762793 }, + { url = "https://files.pythonhosted.org/packages/12/46/de1fbd0c1b5ccaa7f9a005b66761533e2f6a3e560096682683a223631fe9/numpy-2.0.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26df23238872200f63518dd2aa984cfca675d82469535dc7162dc2ee52d9dd5c", size = 19334885 }, + { url = "https://files.pythonhosted.org/packages/cc/dc/d330a6faefd92b446ec0f0dfea4c3207bb1fef3c4771d19cf4543efd2c78/numpy-2.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a46288ec55ebbd58947d31d72be2c63cbf839f0a63b49cb755022310792a3385", size = 15828784 }, +] + +[[package]] +name = "numpy" +version = "2.2.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3e/ed6db5be21ce87955c0cbd3009f2803f59fa08df21b5df06862e2d8e2bdd/numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb", size = 21165245 }, + { url = "https://files.pythonhosted.org/packages/22/c2/4b9221495b2a132cc9d2eb862e21d42a009f5a60e45fc44b00118c174bff/numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90", size = 14360048 }, + { url = "https://files.pythonhosted.org/packages/fd/77/dc2fcfc66943c6410e2bf598062f5959372735ffda175b39906d54f02349/numpy-2.2.6-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163", size = 5340542 }, + { url = "https://files.pythonhosted.org/packages/7a/4f/1cb5fdc353a5f5cc7feb692db9b8ec2c3d6405453f982435efc52561df58/numpy-2.2.6-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf", size = 6878301 }, + { url = "https://files.pythonhosted.org/packages/eb/17/96a3acd228cec142fcb8723bd3cc39c2a474f7dcf0a5d16731980bcafa95/numpy-2.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83", size = 14297320 }, + { url = "https://files.pythonhosted.org/packages/b4/63/3de6a34ad7ad6646ac7d2f55ebc6ad439dbbf9c4370017c50cf403fb19b5/numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915", size = 16801050 }, + { url = "https://files.pythonhosted.org/packages/07/b6/89d837eddef52b3d0cec5c6ba0456c1bf1b9ef6a6672fc2b7873c3ec4e2e/numpy-2.2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680", size = 15807034 }, + { url = "https://files.pythonhosted.org/packages/01/c8/dc6ae86e3c61cfec1f178e5c9f7858584049b6093f843bca541f94120920/numpy-2.2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289", size = 18614185 }, + { url = "https://files.pythonhosted.org/packages/5b/c5/0064b1b7e7c89137b471ccec1fd2282fceaae0ab3a9550f2568782d80357/numpy-2.2.6-cp310-cp310-win32.whl", hash = "sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d", size = 6527149 }, + { url = "https://files.pythonhosted.org/packages/a3/dd/4b822569d6b96c39d1215dbae0582fd99954dcbcf0c1a13c61783feaca3f/numpy-2.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3", size = 12904620 }, + { url = "https://files.pythonhosted.org/packages/da/a8/4f83e2aa666a9fbf56d6118faaaf5f1974d456b1823fda0a176eff722839/numpy-2.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae", size = 21176963 }, + { url = "https://files.pythonhosted.org/packages/b3/2b/64e1affc7972decb74c9e29e5649fac940514910960ba25cd9af4488b66c/numpy-2.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a", size = 14406743 }, + { url = "https://files.pythonhosted.org/packages/4a/9f/0121e375000b5e50ffdd8b25bf78d8e1a5aa4cca3f185d41265198c7b834/numpy-2.2.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42", size = 5352616 }, + { url = "https://files.pythonhosted.org/packages/31/0d/b48c405c91693635fbe2dcd7bc84a33a602add5f63286e024d3b6741411c/numpy-2.2.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491", size = 6889579 }, + { url = "https://files.pythonhosted.org/packages/52/b8/7f0554d49b565d0171eab6e99001846882000883998e7b7d9f0d98b1f934/numpy-2.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a", size = 14312005 }, + { url = "https://files.pythonhosted.org/packages/b3/dd/2238b898e51bd6d389b7389ffb20d7f4c10066d80351187ec8e303a5a475/numpy-2.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf", size = 16821570 }, + { url = "https://files.pythonhosted.org/packages/83/6c/44d0325722cf644f191042bf47eedad61c1e6df2432ed65cbe28509d404e/numpy-2.2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1", size = 15818548 }, + { url = "https://files.pythonhosted.org/packages/ae/9d/81e8216030ce66be25279098789b665d49ff19eef08bfa8cb96d4957f422/numpy-2.2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab", size = 18620521 }, + { url = "https://files.pythonhosted.org/packages/6a/fd/e19617b9530b031db51b0926eed5345ce8ddc669bb3bc0044b23e275ebe8/numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47", size = 6525866 }, + { url = "https://files.pythonhosted.org/packages/31/0a/f354fb7176b81747d870f7991dc763e157a934c717b67b58456bc63da3df/numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303", size = 12907455 }, + { url = "https://files.pythonhosted.org/packages/82/5d/c00588b6cf18e1da539b45d3598d3557084990dcc4331960c15ee776ee41/numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff", size = 20875348 }, + { url = "https://files.pythonhosted.org/packages/66/ee/560deadcdde6c2f90200450d5938f63a34b37e27ebff162810f716f6a230/numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c", size = 14119362 }, + { url = "https://files.pythonhosted.org/packages/3c/65/4baa99f1c53b30adf0acd9a5519078871ddde8d2339dc5a7fde80d9d87da/numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3", size = 5084103 }, + { url = "https://files.pythonhosted.org/packages/cc/89/e5a34c071a0570cc40c9a54eb472d113eea6d002e9ae12bb3a8407fb912e/numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282", size = 6625382 }, + { url = "https://files.pythonhosted.org/packages/f8/35/8c80729f1ff76b3921d5c9487c7ac3de9b2a103b1cd05e905b3090513510/numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87", size = 14018462 }, + { url = "https://files.pythonhosted.org/packages/8c/3d/1e1db36cfd41f895d266b103df00ca5b3cbe965184df824dec5c08c6b803/numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249", size = 16527618 }, + { url = "https://files.pythonhosted.org/packages/61/c6/03ed30992602c85aa3cd95b9070a514f8b3c33e31124694438d88809ae36/numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49", size = 15505511 }, + { url = "https://files.pythonhosted.org/packages/b7/25/5761d832a81df431e260719ec45de696414266613c9ee268394dd5ad8236/numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de", size = 18313783 }, + { url = "https://files.pythonhosted.org/packages/57/0a/72d5a3527c5ebffcd47bde9162c39fae1f90138c961e5296491ce778e682/numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4", size = 6246506 }, + { url = "https://files.pythonhosted.org/packages/36/fa/8c9210162ca1b88529ab76b41ba02d433fd54fecaf6feb70ef9f124683f1/numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2", size = 12614190 }, + { url = "https://files.pythonhosted.org/packages/f9/5c/6657823f4f594f72b5471f1db1ab12e26e890bb2e41897522d134d2a3e81/numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84", size = 20867828 }, + { url = "https://files.pythonhosted.org/packages/dc/9e/14520dc3dadf3c803473bd07e9b2bd1b69bc583cb2497b47000fed2fa92f/numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b", size = 14143006 }, + { url = "https://files.pythonhosted.org/packages/4f/06/7e96c57d90bebdce9918412087fc22ca9851cceaf5567a45c1f404480e9e/numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d", size = 5076765 }, + { url = "https://files.pythonhosted.org/packages/73/ed/63d920c23b4289fdac96ddbdd6132e9427790977d5457cd132f18e76eae0/numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566", size = 6617736 }, + { url = "https://files.pythonhosted.org/packages/85/c5/e19c8f99d83fd377ec8c7e0cf627a8049746da54afc24ef0a0cb73d5dfb5/numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f", size = 14010719 }, + { url = "https://files.pythonhosted.org/packages/19/49/4df9123aafa7b539317bf6d342cb6d227e49f7a35b99c287a6109b13dd93/numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f", size = 16526072 }, + { url = "https://files.pythonhosted.org/packages/b2/6c/04b5f47f4f32f7c2b0e7260442a8cbcf8168b0e1a41ff1495da42f42a14f/numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868", size = 15503213 }, + { url = "https://files.pythonhosted.org/packages/17/0a/5cd92e352c1307640d5b6fec1b2ffb06cd0dabe7d7b8227f97933d378422/numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d", size = 18316632 }, + { url = "https://files.pythonhosted.org/packages/f0/3b/5cba2b1d88760ef86596ad0f3d484b1cbff7c115ae2429678465057c5155/numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd", size = 6244532 }, + { url = "https://files.pythonhosted.org/packages/cb/3b/d58c12eafcb298d4e6d0d40216866ab15f59e55d148a5658bb3132311fcf/numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c", size = 12610885 }, + { url = "https://files.pythonhosted.org/packages/6b/9e/4bf918b818e516322db999ac25d00c75788ddfd2d2ade4fa66f1f38097e1/numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6", size = 20963467 }, + { url = "https://files.pythonhosted.org/packages/61/66/d2de6b291507517ff2e438e13ff7b1e2cdbdb7cb40b3ed475377aece69f9/numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda", size = 14225144 }, + { url = "https://files.pythonhosted.org/packages/e4/25/480387655407ead912e28ba3a820bc69af9adf13bcbe40b299d454ec011f/numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40", size = 5200217 }, + { url = "https://files.pythonhosted.org/packages/aa/4a/6e313b5108f53dcbf3aca0c0f3e9c92f4c10ce57a0a721851f9785872895/numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8", size = 6712014 }, + { url = "https://files.pythonhosted.org/packages/b7/30/172c2d5c4be71fdf476e9de553443cf8e25feddbe185e0bd88b096915bcc/numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f", size = 14077935 }, + { url = "https://files.pythonhosted.org/packages/12/fb/9e743f8d4e4d3c710902cf87af3512082ae3d43b945d5d16563f26ec251d/numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa", size = 16600122 }, + { url = "https://files.pythonhosted.org/packages/12/75/ee20da0e58d3a66f204f38916757e01e33a9737d0b22373b3eb5a27358f9/numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571", size = 15586143 }, + { url = "https://files.pythonhosted.org/packages/76/95/bef5b37f29fc5e739947e9ce5179ad402875633308504a52d188302319c8/numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1", size = 18385260 }, + { url = "https://files.pythonhosted.org/packages/09/04/f2f83279d287407cf36a7a8053a5abe7be3622a4363337338f2585e4afda/numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff", size = 6377225 }, + { url = "https://files.pythonhosted.org/packages/67/0e/35082d13c09c02c011cf21570543d202ad929d961c02a147493cb0c2bdf5/numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06", size = 12771374 }, + { url = "https://files.pythonhosted.org/packages/9e/3b/d94a75f4dbf1ef5d321523ecac21ef23a3cd2ac8b78ae2aac40873590229/numpy-2.2.6-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d", size = 21040391 }, + { url = "https://files.pythonhosted.org/packages/17/f4/09b2fa1b58f0fb4f7c7963a1649c64c4d315752240377ed74d9cd878f7b5/numpy-2.2.6-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db", size = 6786754 }, + { url = "https://files.pythonhosted.org/packages/af/30/feba75f143bdc868a1cc3f44ccfa6c4b9ec522b36458e738cd00f67b573f/numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543", size = 16643476 }, + { url = "https://files.pythonhosted.org/packages/37/48/ac2a9584402fb6c0cd5b5d1a91dcf176b15760130dd386bbafdbfe3640bf/numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00", size = 12812666 }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469 }, +] + +[[package]] +name = "pathos" +version = "0.3.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dill" }, + { name = "multiprocess" }, + { name = "pox" }, + { name = "ppft" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/be/90/fdbe3bbfe79933db439e1844083cb6e9d5a9d3b686738549b3d22d06eae7/pathos-0.3.4.tar.gz", hash = "sha256:bad4912d0ef865654a7cc478da65f2e1d5b69f3d92c4a7d9c9845657783c0754", size = 167076 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/0a/daece46e65c821d153746566a1604ac90338f0279b1fb858a3617eb60472/pathos-0.3.4-py3-none-any.whl", hash = "sha256:fe44883448c05c80d518b61df491b496f6190bb6860253f3254d8c9afb53c340", size = 82261 }, +] + +[[package]] +name = "pathspec" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191 }, +] + +[[package]] +name = "pillow" +version = "11.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/d0d6dea55cd152ce3d6767bb38a8fc10e33796ba4ba210cbab9354b6d238/pillow-11.3.0.tar.gz", hash = "sha256:3828ee7586cd0b2091b6209e5ad53e20d0649bbe87164a459d0676e035e8f523", size = 47113069 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/5d/45a3553a253ac8763f3561371432a90bdbe6000fbdcf1397ffe502aa206c/pillow-11.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1b9c17fd4ace828b3003dfd1e30bff24863e0eb59b535e8f80194d9cc7ecf860", size = 5316554 }, + { url = "https://files.pythonhosted.org/packages/7c/c8/67c12ab069ef586a25a4a79ced553586748fad100c77c0ce59bb4983ac98/pillow-11.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:65dc69160114cdd0ca0f35cb434633c75e8e7fad4cf855177a05bf38678f73ad", size = 4686548 }, + { url = "https://files.pythonhosted.org/packages/2f/bd/6741ebd56263390b382ae4c5de02979af7f8bd9807346d068700dd6d5cf9/pillow-11.3.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7107195ddc914f656c7fc8e4a5e1c25f32e9236ea3ea860f257b0436011fddd0", size = 5859742 }, + { url = "https://files.pythonhosted.org/packages/ca/0b/c412a9e27e1e6a829e6ab6c2dca52dd563efbedf4c9c6aa453d9a9b77359/pillow-11.3.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc3e831b563b3114baac7ec2ee86819eb03caa1a2cef0b481a5675b59c4fe23b", size = 7633087 }, + { url = "https://files.pythonhosted.org/packages/59/9d/9b7076aaf30f5dd17e5e5589b2d2f5a5d7e30ff67a171eb686e4eecc2adf/pillow-11.3.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f1f182ebd2303acf8c380a54f615ec883322593320a9b00438eb842c1f37ae50", size = 5963350 }, + { url = "https://files.pythonhosted.org/packages/f0/16/1a6bf01fb622fb9cf5c91683823f073f053005c849b1f52ed613afcf8dae/pillow-11.3.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4445fa62e15936a028672fd48c4c11a66d641d2c05726c7ec1f8ba6a572036ae", size = 6631840 }, + { url = "https://files.pythonhosted.org/packages/7b/e6/6ff7077077eb47fde78739e7d570bdcd7c10495666b6afcd23ab56b19a43/pillow-11.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:71f511f6b3b91dd543282477be45a033e4845a40278fa8dcdbfdb07109bf18f9", size = 6074005 }, + { url = "https://files.pythonhosted.org/packages/c3/3a/b13f36832ea6d279a697231658199e0a03cd87ef12048016bdcc84131601/pillow-11.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:040a5b691b0713e1f6cbe222e0f4f74cd233421e105850ae3b3c0ceda520f42e", size = 6708372 }, + { url = "https://files.pythonhosted.org/packages/6c/e4/61b2e1a7528740efbc70b3d581f33937e38e98ef3d50b05007267a55bcb2/pillow-11.3.0-cp310-cp310-win32.whl", hash = "sha256:89bd777bc6624fe4115e9fac3352c79ed60f3bb18651420635f26e643e3dd1f6", size = 6277090 }, + { url = "https://files.pythonhosted.org/packages/a9/d3/60c781c83a785d6afbd6a326ed4d759d141de43aa7365725cbcd65ce5e54/pillow-11.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:19d2ff547c75b8e3ff46f4d9ef969a06c30ab2d4263a9e287733aa8b2429ce8f", size = 6985988 }, + { url = "https://files.pythonhosted.org/packages/9f/28/4f4a0203165eefb3763939c6789ba31013a2e90adffb456610f30f613850/pillow-11.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:819931d25e57b513242859ce1876c58c59dc31587847bf74cfe06b2e0cb22d2f", size = 2422899 }, + { url = "https://files.pythonhosted.org/packages/db/26/77f8ed17ca4ffd60e1dcd220a6ec6d71210ba398cfa33a13a1cd614c5613/pillow-11.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:1cd110edf822773368b396281a2293aeb91c90a2db00d78ea43e7e861631b722", size = 5316531 }, + { url = "https://files.pythonhosted.org/packages/cb/39/ee475903197ce709322a17a866892efb560f57900d9af2e55f86db51b0a5/pillow-11.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9c412fddd1b77a75aa904615ebaa6001f169b26fd467b4be93aded278266b288", size = 4686560 }, + { url = "https://files.pythonhosted.org/packages/d5/90/442068a160fd179938ba55ec8c97050a612426fae5ec0a764e345839f76d/pillow-11.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1aa4de119a0ecac0a34a9c8bde33f34022e2e8f99104e47a3ca392fd60e37d", size = 5870978 }, + { url = "https://files.pythonhosted.org/packages/13/92/dcdd147ab02daf405387f0218dcf792dc6dd5b14d2573d40b4caeef01059/pillow-11.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:91da1d88226663594e3f6b4b8c3c8d85bd504117d043740a8e0ec449087cc494", size = 7641168 }, + { url = "https://files.pythonhosted.org/packages/6e/db/839d6ba7fd38b51af641aa904e2960e7a5644d60ec754c046b7d2aee00e5/pillow-11.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:643f189248837533073c405ec2f0bb250ba54598cf80e8c1e043381a60632f58", size = 5973053 }, + { url = "https://files.pythonhosted.org/packages/f2/2f/d7675ecae6c43e9f12aa8d58b6012683b20b6edfbdac7abcb4e6af7a3784/pillow-11.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:106064daa23a745510dabce1d84f29137a37224831d88eb4ce94bb187b1d7e5f", size = 6640273 }, + { url = "https://files.pythonhosted.org/packages/45/ad/931694675ede172e15b2ff03c8144a0ddaea1d87adb72bb07655eaffb654/pillow-11.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cd8ff254faf15591e724dc7c4ddb6bf4793efcbe13802a4ae3e863cd300b493e", size = 6082043 }, + { url = "https://files.pythonhosted.org/packages/3a/04/ba8f2b11fc80d2dd462d7abec16351b45ec99cbbaea4387648a44190351a/pillow-11.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:932c754c2d51ad2b2271fd01c3d121daaa35e27efae2a616f77bf164bc0b3e94", size = 6715516 }, + { url = "https://files.pythonhosted.org/packages/48/59/8cd06d7f3944cc7d892e8533c56b0acb68399f640786313275faec1e3b6f/pillow-11.3.0-cp311-cp311-win32.whl", hash = "sha256:b4b8f3efc8d530a1544e5962bd6b403d5f7fe8b9e08227c6b255f98ad82b4ba0", size = 6274768 }, + { url = "https://files.pythonhosted.org/packages/f1/cc/29c0f5d64ab8eae20f3232da8f8571660aa0ab4b8f1331da5c2f5f9a938e/pillow-11.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:1a992e86b0dd7aeb1f053cd506508c0999d710a8f07b4c791c63843fc6a807ac", size = 6986055 }, + { url = "https://files.pythonhosted.org/packages/c6/df/90bd886fabd544c25addd63e5ca6932c86f2b701d5da6c7839387a076b4a/pillow-11.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:30807c931ff7c095620fe04448e2c2fc673fcbb1ffe2a7da3fb39613489b1ddd", size = 2423079 }, + { url = "https://files.pythonhosted.org/packages/40/fe/1bc9b3ee13f68487a99ac9529968035cca2f0a51ec36892060edcc51d06a/pillow-11.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdae223722da47b024b867c1ea0be64e0df702c5e0a60e27daad39bf960dd1e4", size = 5278800 }, + { url = "https://files.pythonhosted.org/packages/2c/32/7e2ac19b5713657384cec55f89065fb306b06af008cfd87e572035b27119/pillow-11.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:921bd305b10e82b4d1f5e802b6850677f965d8394203d182f078873851dada69", size = 4686296 }, + { url = "https://files.pythonhosted.org/packages/8e/1e/b9e12bbe6e4c2220effebc09ea0923a07a6da1e1f1bfbc8d7d29a01ce32b/pillow-11.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb76541cba2f958032d79d143b98a3a6b3ea87f0959bbe256c0b5e416599fd5d", size = 5871726 }, + { url = "https://files.pythonhosted.org/packages/8d/33/e9200d2bd7ba00dc3ddb78df1198a6e80d7669cce6c2bdbeb2530a74ec58/pillow-11.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:67172f2944ebba3d4a7b54f2e95c786a3a50c21b88456329314caaa28cda70f6", size = 7644652 }, + { url = "https://files.pythonhosted.org/packages/41/f1/6f2427a26fc683e00d985bc391bdd76d8dd4e92fac33d841127eb8fb2313/pillow-11.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f07ed9f56a3b9b5f49d3661dc9607484e85c67e27f3e8be2c7d28ca032fec7", size = 5977787 }, + { url = "https://files.pythonhosted.org/packages/e4/c9/06dd4a38974e24f932ff5f98ea3c546ce3f8c995d3f0985f8e5ba48bba19/pillow-11.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:676b2815362456b5b3216b4fd5bd89d362100dc6f4945154ff172e206a22c024", size = 6645236 }, + { url = "https://files.pythonhosted.org/packages/40/e7/848f69fb79843b3d91241bad658e9c14f39a32f71a301bcd1d139416d1be/pillow-11.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3e184b2f26ff146363dd07bde8b711833d7b0202e27d13540bfe2e35a323a809", size = 6086950 }, + { url = "https://files.pythonhosted.org/packages/0b/1a/7cff92e695a2a29ac1958c2a0fe4c0b2393b60aac13b04a4fe2735cad52d/pillow-11.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6be31e3fc9a621e071bc17bb7de63b85cbe0bfae91bb0363c893cbe67247780d", size = 6723358 }, + { url = "https://files.pythonhosted.org/packages/26/7d/73699ad77895f69edff76b0f332acc3d497f22f5d75e5360f78cbcaff248/pillow-11.3.0-cp312-cp312-win32.whl", hash = "sha256:7b161756381f0918e05e7cb8a371fff367e807770f8fe92ecb20d905d0e1c149", size = 6275079 }, + { url = "https://files.pythonhosted.org/packages/8c/ce/e7dfc873bdd9828f3b6e5c2bbb74e47a98ec23cc5c74fc4e54462f0d9204/pillow-11.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a6444696fce635783440b7f7a9fc24b3ad10a9ea3f0ab66c5905be1c19ccf17d", size = 6986324 }, + { url = "https://files.pythonhosted.org/packages/16/8f/b13447d1bf0b1f7467ce7d86f6e6edf66c0ad7cf44cf5c87a37f9bed9936/pillow-11.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:2aceea54f957dd4448264f9bf40875da0415c83eb85f55069d89c0ed436e3542", size = 2423067 }, + { url = "https://files.pythonhosted.org/packages/1e/93/0952f2ed8db3a5a4c7a11f91965d6184ebc8cd7cbb7941a260d5f018cd2d/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:1c627742b539bba4309df89171356fcb3cc5a9178355b2727d1b74a6cf155fbd", size = 2128328 }, + { url = "https://files.pythonhosted.org/packages/4b/e8/100c3d114b1a0bf4042f27e0f87d2f25e857e838034e98ca98fe7b8c0a9c/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:30b7c02f3899d10f13d7a48163c8969e4e653f8b43416d23d13d1bbfdc93b9f8", size = 2170652 }, + { url = "https://files.pythonhosted.org/packages/aa/86/3f758a28a6e381758545f7cdb4942e1cb79abd271bea932998fc0db93cb6/pillow-11.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7859a4cc7c9295f5838015d8cc0a9c215b77e43d07a25e460f35cf516df8626f", size = 2227443 }, + { url = "https://files.pythonhosted.org/packages/01/f4/91d5b3ffa718df2f53b0dc109877993e511f4fd055d7e9508682e8aba092/pillow-11.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec1ee50470b0d050984394423d96325b744d55c701a439d2bd66089bff963d3c", size = 5278474 }, + { url = "https://files.pythonhosted.org/packages/f9/0e/37d7d3eca6c879fbd9dba21268427dffda1ab00d4eb05b32923d4fbe3b12/pillow-11.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7db51d222548ccfd274e4572fdbf3e810a5e66b00608862f947b163e613b67dd", size = 4686038 }, + { url = "https://files.pythonhosted.org/packages/ff/b0/3426e5c7f6565e752d81221af9d3676fdbb4f352317ceafd42899aaf5d8a/pillow-11.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2d6fcc902a24ac74495df63faad1884282239265c6839a0a6416d33faedfae7e", size = 5864407 }, + { url = "https://files.pythonhosted.org/packages/fc/c1/c6c423134229f2a221ee53f838d4be9d82bab86f7e2f8e75e47b6bf6cd77/pillow-11.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f0f5d8f4a08090c6d6d578351a2b91acf519a54986c055af27e7a93feae6d3f1", size = 7639094 }, + { url = "https://files.pythonhosted.org/packages/ba/c9/09e6746630fe6372c67c648ff9deae52a2bc20897d51fa293571977ceb5d/pillow-11.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c37d8ba9411d6003bba9e518db0db0c58a680ab9fe5179f040b0463644bc9805", size = 5973503 }, + { url = "https://files.pythonhosted.org/packages/d5/1c/a2a29649c0b1983d3ef57ee87a66487fdeb45132df66ab30dd37f7dbe162/pillow-11.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13f87d581e71d9189ab21fe0efb5a23e9f28552d5be6979e84001d3b8505abe8", size = 6642574 }, + { url = "https://files.pythonhosted.org/packages/36/de/d5cc31cc4b055b6c6fd990e3e7f0f8aaf36229a2698501bcb0cdf67c7146/pillow-11.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:023f6d2d11784a465f09fd09a34b150ea4672e85fb3d05931d89f373ab14abb2", size = 6084060 }, + { url = "https://files.pythonhosted.org/packages/d5/ea/502d938cbaeec836ac28a9b730193716f0114c41325db428e6b280513f09/pillow-11.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:45dfc51ac5975b938e9809451c51734124e73b04d0f0ac621649821a63852e7b", size = 6721407 }, + { url = "https://files.pythonhosted.org/packages/45/9c/9c5e2a73f125f6cbc59cc7087c8f2d649a7ae453f83bd0362ff7c9e2aee2/pillow-11.3.0-cp313-cp313-win32.whl", hash = "sha256:a4d336baed65d50d37b88ca5b60c0fa9d81e3a87d4a7930d3880d1624d5b31f3", size = 6273841 }, + { url = "https://files.pythonhosted.org/packages/23/85/397c73524e0cd212067e0c969aa245b01d50183439550d24d9f55781b776/pillow-11.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0bce5c4fd0921f99d2e858dc4d4d64193407e1b99478bc5cacecba2311abde51", size = 6978450 }, + { url = "https://files.pythonhosted.org/packages/17/d2/622f4547f69cd173955194b78e4d19ca4935a1b0f03a302d655c9f6aae65/pillow-11.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:1904e1264881f682f02b7f8167935cce37bc97db457f8e7849dc3a6a52b99580", size = 2423055 }, + { url = "https://files.pythonhosted.org/packages/dd/80/a8a2ac21dda2e82480852978416cfacd439a4b490a501a288ecf4fe2532d/pillow-11.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4c834a3921375c48ee6b9624061076bc0a32a60b5532b322cc0ea64e639dd50e", size = 5281110 }, + { url = "https://files.pythonhosted.org/packages/44/d6/b79754ca790f315918732e18f82a8146d33bcd7f4494380457ea89eb883d/pillow-11.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5e05688ccef30ea69b9317a9ead994b93975104a677a36a8ed8106be9260aa6d", size = 4689547 }, + { url = "https://files.pythonhosted.org/packages/49/20/716b8717d331150cb00f7fdd78169c01e8e0c219732a78b0e59b6bdb2fd6/pillow-11.3.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1019b04af07fc0163e2810167918cb5add8d74674b6267616021ab558dc98ced", size = 5901554 }, + { url = "https://files.pythonhosted.org/packages/74/cf/a9f3a2514a65bb071075063a96f0a5cf949c2f2fce683c15ccc83b1c1cab/pillow-11.3.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f944255db153ebb2b19c51fe85dd99ef0ce494123f21b9db4877ffdfc5590c7c", size = 7669132 }, + { url = "https://files.pythonhosted.org/packages/98/3c/da78805cbdbee9cb43efe8261dd7cc0b4b93f2ac79b676c03159e9db2187/pillow-11.3.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f85acb69adf2aaee8b7da124efebbdb959a104db34d3a2cb0f3793dbae422a8", size = 6005001 }, + { url = "https://files.pythonhosted.org/packages/6c/fa/ce044b91faecf30e635321351bba32bab5a7e034c60187fe9698191aef4f/pillow-11.3.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05f6ecbeff5005399bb48d198f098a9b4b6bdf27b8487c7f38ca16eeb070cd59", size = 6668814 }, + { url = "https://files.pythonhosted.org/packages/7b/51/90f9291406d09bf93686434f9183aba27b831c10c87746ff49f127ee80cb/pillow-11.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a7bc6e6fd0395bc052f16b1a8670859964dbd7003bd0af2ff08342eb6e442cfe", size = 6113124 }, + { url = "https://files.pythonhosted.org/packages/cd/5a/6fec59b1dfb619234f7636d4157d11fb4e196caeee220232a8d2ec48488d/pillow-11.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:83e1b0161c9d148125083a35c1c5a89db5b7054834fd4387499e06552035236c", size = 6747186 }, + { url = "https://files.pythonhosted.org/packages/49/6b/00187a044f98255225f172de653941e61da37104a9ea60e4f6887717e2b5/pillow-11.3.0-cp313-cp313t-win32.whl", hash = "sha256:2a3117c06b8fb646639dce83694f2f9eac405472713fcb1ae887469c0d4f6788", size = 6277546 }, + { url = "https://files.pythonhosted.org/packages/e8/5c/6caaba7e261c0d75bab23be79f1d06b5ad2a2ae49f028ccec801b0e853d6/pillow-11.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:857844335c95bea93fb39e0fa2726b4d9d758850b34075a7e3ff4f4fa3aa3b31", size = 6985102 }, + { url = "https://files.pythonhosted.org/packages/f3/7e/b623008460c09a0cb38263c93b828c666493caee2eb34ff67f778b87e58c/pillow-11.3.0-cp313-cp313t-win_arm64.whl", hash = "sha256:8797edc41f3e8536ae4b10897ee2f637235c94f27404cac7297f7b607dd0716e", size = 2424803 }, + { url = "https://files.pythonhosted.org/packages/73/f4/04905af42837292ed86cb1b1dabe03dce1edc008ef14c473c5c7e1443c5d/pillow-11.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d9da3df5f9ea2a89b81bb6087177fb1f4d1c7146d583a3fe5c672c0d94e55e12", size = 5278520 }, + { url = "https://files.pythonhosted.org/packages/41/b0/33d79e377a336247df6348a54e6d2a2b85d644ca202555e3faa0cf811ecc/pillow-11.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0b275ff9b04df7b640c59ec5a3cb113eefd3795a8df80bac69646ef699c6981a", size = 4686116 }, + { url = "https://files.pythonhosted.org/packages/49/2d/ed8bc0ab219ae8768f529597d9509d184fe8a6c4741a6864fea334d25f3f/pillow-11.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0743841cabd3dba6a83f38a92672cccbd69af56e3e91777b0ee7f4dba4385632", size = 5864597 }, + { url = "https://files.pythonhosted.org/packages/b5/3d/b932bb4225c80b58dfadaca9d42d08d0b7064d2d1791b6a237f87f661834/pillow-11.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2465a69cf967b8b49ee1b96d76718cd98c4e925414ead59fdf75cf0fd07df673", size = 7638246 }, + { url = "https://files.pythonhosted.org/packages/09/b5/0487044b7c096f1b48f0d7ad416472c02e0e4bf6919541b111efd3cae690/pillow-11.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41742638139424703b4d01665b807c6468e23e699e8e90cffefe291c5832b027", size = 5973336 }, + { url = "https://files.pythonhosted.org/packages/a8/2d/524f9318f6cbfcc79fbc004801ea6b607ec3f843977652fdee4857a7568b/pillow-11.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93efb0b4de7e340d99057415c749175e24c8864302369e05914682ba642e5d77", size = 6642699 }, + { url = "https://files.pythonhosted.org/packages/6f/d2/a9a4f280c6aefedce1e8f615baaa5474e0701d86dd6f1dede66726462bbd/pillow-11.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7966e38dcd0fa11ca390aed7c6f20454443581d758242023cf36fcb319b1a874", size = 6083789 }, + { url = "https://files.pythonhosted.org/packages/fe/54/86b0cd9dbb683a9d5e960b66c7379e821a19be4ac5810e2e5a715c09a0c0/pillow-11.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:98a9afa7b9007c67ed84c57c9e0ad86a6000da96eaa638e4f8abe5b65ff83f0a", size = 6720386 }, + { url = "https://files.pythonhosted.org/packages/e7/95/88efcaf384c3588e24259c4203b909cbe3e3c2d887af9e938c2022c9dd48/pillow-11.3.0-cp314-cp314-win32.whl", hash = "sha256:02a723e6bf909e7cea0dac1b0e0310be9d7650cd66222a5f1c571455c0a45214", size = 6370911 }, + { url = "https://files.pythonhosted.org/packages/2e/cc/934e5820850ec5eb107e7b1a72dd278140731c669f396110ebc326f2a503/pillow-11.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:a418486160228f64dd9e9efcd132679b7a02a5f22c982c78b6fc7dab3fefb635", size = 7117383 }, + { url = "https://files.pythonhosted.org/packages/d6/e9/9c0a616a71da2a5d163aa37405e8aced9a906d574b4a214bede134e731bc/pillow-11.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:155658efb5e044669c08896c0c44231c5e9abcaadbc5cd3648df2f7c0b96b9a6", size = 2511385 }, + { url = "https://files.pythonhosted.org/packages/1a/33/c88376898aff369658b225262cd4f2659b13e8178e7534df9e6e1fa289f6/pillow-11.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:59a03cdf019efbfeeed910bf79c7c93255c3d54bc45898ac2a4140071b02b4ae", size = 5281129 }, + { url = "https://files.pythonhosted.org/packages/1f/70/d376247fb36f1844b42910911c83a02d5544ebd2a8bad9efcc0f707ea774/pillow-11.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f8a5827f84d973d8636e9dc5764af4f0cf2318d26744b3d902931701b0d46653", size = 4689580 }, + { url = "https://files.pythonhosted.org/packages/eb/1c/537e930496149fbac69efd2fc4329035bbe2e5475b4165439e3be9cb183b/pillow-11.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ee92f2fd10f4adc4b43d07ec5e779932b4eb3dbfbc34790ada5a6669bc095aa6", size = 5902860 }, + { url = "https://files.pythonhosted.org/packages/bd/57/80f53264954dcefeebcf9dae6e3eb1daea1b488f0be8b8fef12f79a3eb10/pillow-11.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c96d333dcf42d01f47b37e0979b6bd73ec91eae18614864622d9b87bbd5bbf36", size = 7670694 }, + { url = "https://files.pythonhosted.org/packages/70/ff/4727d3b71a8578b4587d9c276e90efad2d6fe0335fd76742a6da08132e8c/pillow-11.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c96f993ab8c98460cd0c001447bff6194403e8b1d7e149ade5f00594918128b", size = 6005888 }, + { url = "https://files.pythonhosted.org/packages/05/ae/716592277934f85d3be51d7256f3636672d7b1abfafdc42cf3f8cbd4b4c8/pillow-11.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41342b64afeba938edb034d122b2dda5db2139b9a4af999729ba8818e0056477", size = 6670330 }, + { url = "https://files.pythonhosted.org/packages/e7/bb/7fe6cddcc8827b01b1a9766f5fdeb7418680744f9082035bdbabecf1d57f/pillow-11.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:068d9c39a2d1b358eb9f245ce7ab1b5c3246c7c8c7d9ba58cfa5b43146c06e50", size = 6114089 }, + { url = "https://files.pythonhosted.org/packages/8b/f5/06bfaa444c8e80f1a8e4bff98da9c83b37b5be3b1deaa43d27a0db37ef84/pillow-11.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a1bc6ba083b145187f648b667e05a2534ecc4b9f2784c2cbe3089e44868f2b9b", size = 6748206 }, + { url = "https://files.pythonhosted.org/packages/f0/77/bc6f92a3e8e6e46c0ca78abfffec0037845800ea38c73483760362804c41/pillow-11.3.0-cp314-cp314t-win32.whl", hash = "sha256:118ca10c0d60b06d006be10a501fd6bbdfef559251ed31b794668ed569c87e12", size = 6377370 }, + { url = "https://files.pythonhosted.org/packages/4a/82/3a721f7d69dca802befb8af08b7c79ebcab461007ce1c18bd91a5d5896f9/pillow-11.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8924748b688aa210d79883357d102cd64690e56b923a186f35a82cbc10f997db", size = 7121500 }, + { url = "https://files.pythonhosted.org/packages/89/c7/5572fa4a3f45740eaab6ae86fcdf7195b55beac1371ac8c619d880cfe948/pillow-11.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:79ea0d14d3ebad43ec77ad5272e6ff9bba5b679ef73375ea760261207fa8e0aa", size = 2512835 }, + { url = "https://files.pythonhosted.org/packages/9e/8e/9c089f01677d1264ab8648352dcb7773f37da6ad002542760c80107da816/pillow-11.3.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:48d254f8a4c776de343051023eb61ffe818299eeac478da55227d96e241de53f", size = 5316478 }, + { url = "https://files.pythonhosted.org/packages/b5/a9/5749930caf674695867eb56a581e78eb5f524b7583ff10b01b6e5048acb3/pillow-11.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7aee118e30a4cf54fdd873bd3a29de51e29105ab11f9aad8c32123f58c8f8081", size = 4686522 }, + { url = "https://files.pythonhosted.org/packages/43/46/0b85b763eb292b691030795f9f6bb6fcaf8948c39413c81696a01c3577f7/pillow-11.3.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:23cff760a9049c502721bdb743a7cb3e03365fafcdfc2ef9784610714166e5a4", size = 5853376 }, + { url = "https://files.pythonhosted.org/packages/5e/c6/1a230ec0067243cbd60bc2dad5dc3ab46a8a41e21c15f5c9b52b26873069/pillow-11.3.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6359a3bc43f57d5b375d1ad54a0074318a0844d11b76abccf478c37c986d3cfc", size = 7626020 }, + { url = "https://files.pythonhosted.org/packages/63/dd/f296c27ffba447bfad76c6a0c44c1ea97a90cb9472b9304c94a732e8dbfb/pillow-11.3.0-cp39-cp39-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:092c80c76635f5ecb10f3f83d76716165c96f5229addbd1ec2bdbbda7d496e06", size = 5956732 }, + { url = "https://files.pythonhosted.org/packages/a5/a0/98a3630f0b57f77bae67716562513d3032ae70414fcaf02750279c389a9e/pillow-11.3.0-cp39-cp39-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cadc9e0ea0a2431124cde7e1697106471fc4c1da01530e679b2391c37d3fbb3a", size = 6624404 }, + { url = "https://files.pythonhosted.org/packages/de/e6/83dfba5646a290edd9a21964da07674409e410579c341fc5b8f7abd81620/pillow-11.3.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:6a418691000f2a418c9135a7cf0d797c1bb7d9a485e61fe8e7722845b95ef978", size = 6067760 }, + { url = "https://files.pythonhosted.org/packages/bc/41/15ab268fe6ee9a2bc7391e2bbb20a98d3974304ab1a406a992dcb297a370/pillow-11.3.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:97afb3a00b65cc0804d1c7abddbf090a81eaac02768af58cbdcaaa0a931e0b6d", size = 6700534 }, + { url = "https://files.pythonhosted.org/packages/64/79/6d4f638b288300bed727ff29f2a3cb63db054b33518a95f27724915e3fbc/pillow-11.3.0-cp39-cp39-win32.whl", hash = "sha256:ea944117a7974ae78059fcc1800e5d3295172bb97035c0c1d9345fca1419da71", size = 6277091 }, + { url = "https://files.pythonhosted.org/packages/46/05/4106422f45a05716fd34ed21763f8ec182e8ea00af6e9cb05b93a247361a/pillow-11.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:e5c5858ad8ec655450a7c7df532e9842cf8df7cc349df7225c60d5d348c8aada", size = 6986091 }, + { url = "https://files.pythonhosted.org/packages/63/c6/287fd55c2c12761d0591549d48885187579b7c257bef0c6660755b0b59ae/pillow-11.3.0-cp39-cp39-win_arm64.whl", hash = "sha256:6abdbfd3aea42be05702a8dd98832329c167ee84400a1d1f61ab11437f1717eb", size = 2422632 }, + { url = "https://files.pythonhosted.org/packages/6f/8b/209bd6b62ce8367f47e68a218bffac88888fdf2c9fcf1ecadc6c3ec1ebc7/pillow-11.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3cee80663f29e3843b68199b9d6f4f54bd1d4a6b59bdd91bceefc51238bcb967", size = 5270556 }, + { url = "https://files.pythonhosted.org/packages/2e/e6/231a0b76070c2cfd9e260a7a5b504fb72da0a95279410fa7afd99d9751d6/pillow-11.3.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b5f56c3f344f2ccaf0dd875d3e180f631dc60a51b314295a3e681fe8cf851fbe", size = 4654625 }, + { url = "https://files.pythonhosted.org/packages/13/f4/10cf94fda33cb12765f2397fc285fa6d8eb9c29de7f3185165b702fc7386/pillow-11.3.0-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e67d793d180c9df62f1f40aee3accca4829d3794c95098887edc18af4b8b780c", size = 4874207 }, + { url = "https://files.pythonhosted.org/packages/72/c9/583821097dc691880c92892e8e2d41fe0a5a3d6021f4963371d2f6d57250/pillow-11.3.0-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d000f46e2917c705e9fb93a3606ee4a819d1e3aa7a9b442f6444f07e77cf5e25", size = 6583939 }, + { url = "https://files.pythonhosted.org/packages/3b/8e/5c9d410f9217b12320efc7c413e72693f48468979a013ad17fd690397b9a/pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:527b37216b6ac3a12d7838dc3bd75208ec57c1c6d11ef01902266a5a0c14fc27", size = 4957166 }, + { url = "https://files.pythonhosted.org/packages/62/bb/78347dbe13219991877ffb3a91bf09da8317fbfcd4b5f9140aeae020ad71/pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:be5463ac478b623b9dd3937afd7fb7ab3d79dd290a28e2b6df292dc75063eb8a", size = 5581482 }, + { url = "https://files.pythonhosted.org/packages/d9/28/1000353d5e61498aaeaaf7f1e4b49ddb05f2c6575f9d4f9f914a3538b6e1/pillow-11.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:8dc70ca24c110503e16918a658b869019126ecfe03109b754c402daff12b3d9f", size = 6984596 }, + { url = "https://files.pythonhosted.org/packages/9e/e3/6fa84033758276fb31da12e5fb66ad747ae83b93c67af17f8c6ff4cc8f34/pillow-11.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7c8ec7a017ad1bd562f93dbd8505763e688d388cde6e4a010ae1486916e713e6", size = 5270566 }, + { url = "https://files.pythonhosted.org/packages/5b/ee/e8d2e1ab4892970b561e1ba96cbd59c0d28cf66737fc44abb2aec3795a4e/pillow-11.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9ab6ae226de48019caa8074894544af5b53a117ccb9d3b3dcb2871464c829438", size = 4654618 }, + { url = "https://files.pythonhosted.org/packages/f2/6d/17f80f4e1f0761f02160fc433abd4109fa1548dcfdca46cfdadaf9efa565/pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fe27fb049cdcca11f11a7bfda64043c37b30e6b91f10cb5bab275806c32f6ab3", size = 4874248 }, + { url = "https://files.pythonhosted.org/packages/de/5f/c22340acd61cef960130585bbe2120e2fd8434c214802f07e8c03596b17e/pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:465b9e8844e3c3519a983d58b80be3f668e2a7a5db97f2784e7079fbc9f9822c", size = 6583963 }, + { url = "https://files.pythonhosted.org/packages/31/5e/03966aedfbfcbb4d5f8aa042452d3361f325b963ebbadddac05b122e47dd/pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5418b53c0d59b3824d05e029669efa023bbef0f3e92e75ec8428f3799487f361", size = 4957170 }, + { url = "https://files.pythonhosted.org/packages/cc/2d/e082982aacc927fc2cab48e1e731bdb1643a1406acace8bed0900a61464e/pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:504b6f59505f08ae014f724b6207ff6222662aab5cc9542577fb084ed0676ac7", size = 5581505 }, + { url = "https://files.pythonhosted.org/packages/34/e7/ae39f538fd6844e982063c3a5e4598b8ced43b9633baa3a85ef33af8c05c/pillow-11.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c84d689db21a1c397d001aa08241044aa2069e7587b398c8cc63020390b1c1b8", size = 6984598 }, +] + +[[package]] +name = "polars" +version = "1.31.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fd/f5/de1b5ecd7d0bd0dd87aa392937f759f9cc3997c5866a9a7f94eabf37cd48/polars-1.31.0.tar.gz", hash = "sha256:59a88054a5fc0135386268ceefdbb6a6cc012d21b5b44fed4f1d3faabbdcbf32", size = 4681224 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/6e/bdd0937653c1e7a564a09ae3bc7757ce83fedbf19da600c8b35d62c0182a/polars-1.31.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:ccc68cd6877deecd46b13cbd2663ca89ab2a2cb1fe49d5cfc66a9cef166566d9", size = 34511354 }, + { url = "https://files.pythonhosted.org/packages/77/fe/81aaca3540c1a5530b4bc4fd7f1b6f77100243d7bb9b7ad3478b770d8b3e/polars-1.31.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:a94c5550df397ad3c2d6adc212e59fd93d9b044ec974dd3653e121e6487a7d21", size = 31377712 }, + { url = "https://files.pythonhosted.org/packages/b8/d9/5e2753784ea30d84b3e769a56f5e50ac5a89c129e87baa16ac0773eb4ef7/polars-1.31.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ada7940ed92bea65d5500ae7ac1f599798149df8faa5a6db150327c9ddbee4f1", size = 35050729 }, + { url = "https://files.pythonhosted.org/packages/20/e8/a6bdfe7b687c1fe84bceb1f854c43415eaf0d2fdf3c679a9dc9c4776e462/polars-1.31.0-cp39-abi3-manylinux_2_24_aarch64.whl", hash = "sha256:b324e6e3e8c6cc6593f9d72fe625f06af65e8d9d47c8686583585533a5e731e1", size = 32260836 }, + { url = "https://files.pythonhosted.org/packages/6e/f6/9d9ad9dc4480d66502497e90ce29efc063373e1598f4bd9b6a38af3e08e7/polars-1.31.0-cp39-abi3-win_amd64.whl", hash = "sha256:3fd874d3432fc932863e8cceff2cff8a12a51976b053f2eb6326a0672134a632", size = 35156211 }, + { url = "https://files.pythonhosted.org/packages/40/4b/0673a68ac4d6527fac951970e929c3b4440c654f994f0c957bd5556deb38/polars-1.31.0-cp39-abi3-win_arm64.whl", hash = "sha256:62ef23bb9d10dca4c2b945979f9a50812ac4ace4ed9e158a6b5d32a7322e6f75", size = 31469078 }, +] + +[[package]] +name = "pox" +version = "0.3.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/56/99/42670d273fd598a6fe98c8b2f593ee425b29e44f2d1a61ff622031204ccd/pox-0.3.6.tar.gz", hash = "sha256:84eeed39600159a62804aacfc00e353edeaae67d8c647ccaaab73a6efed3f605", size = 119393 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/c2/6435789c26661bef699868ee54d2763aea636a1ed21ec8e350b1f9f65888/pox-0.3.6-py3-none-any.whl", hash = "sha256:d48654d0a3dca0c9c02dccae54a53c3870286a5217ad306b2bd94f84e008bc1b", size = 29495 }, +] + +[[package]] +name = "ppft" +version = "1.7.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/46/9e9f2ae7e8e284acbde6ab36f7f4a35b273519a60c0ed419af2da780d49f/ppft-1.7.7.tar.gz", hash = "sha256:f3f77448cfe24c2b8d2296b6d8732280b25041a3f3e1f551856c6451d3e01b96", size = 136272 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/23/6aef7c24f4ee6f765aeaaaa3bf24cfdb0730a20336a02b1a061d227d84be/ppft-1.7.7-py3-none-any.whl", hash = "sha256:fb7524db110682de886b4bb5b08f7bf6a38940566074ef2f62521cbbd3864676", size = 56764 }, +] + +[[package]] +name = "protobuf" +version = "6.31.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/f3/b9655a711b32c19720253f6f06326faf90580834e2e83f840472d752bc8b/protobuf-6.31.1.tar.gz", hash = "sha256:d8cac4c982f0b957a4dc73a80e2ea24fab08e679c0de9deb835f4a12d69aca9a", size = 441797 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/6f/6ab8e4bf962fd5570d3deaa2d5c38f0a363f57b4501047b5ebeb83ab1125/protobuf-6.31.1-cp310-abi3-win32.whl", hash = "sha256:7fa17d5a29c2e04b7d90e5e32388b8bfd0e7107cd8e616feef7ed3fa6bdab5c9", size = 423603 }, + { url = "https://files.pythonhosted.org/packages/44/3a/b15c4347dd4bf3a1b0ee882f384623e2063bb5cf9fa9d57990a4f7df2fb6/protobuf-6.31.1-cp310-abi3-win_amd64.whl", hash = "sha256:426f59d2964864a1a366254fa703b8632dcec0790d8862d30034d8245e1cd447", size = 435283 }, + { url = "https://files.pythonhosted.org/packages/6a/c9/b9689a2a250264a84e66c46d8862ba788ee7a641cdca39bccf64f59284b7/protobuf-6.31.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:6f1227473dc43d44ed644425268eb7c2e488ae245d51c6866d19fe158e207402", size = 425604 }, + { url = "https://files.pythonhosted.org/packages/76/a1/7a5a94032c83375e4fe7e7f56e3976ea6ac90c5e85fac8576409e25c39c3/protobuf-6.31.1-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:a40fc12b84c154884d7d4c4ebd675d5b3b5283e155f324049ae396b95ddebc39", size = 322115 }, + { url = "https://files.pythonhosted.org/packages/fa/b1/b59d405d64d31999244643d88c45c8241c58f17cc887e73bcb90602327f8/protobuf-6.31.1-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:4ee898bf66f7a8b0bd21bce523814e6fbd8c6add948045ce958b73af7e8878c6", size = 321070 }, + { url = "https://files.pythonhosted.org/packages/b1/f0/4160dbd205eee8fdf8647d154e7ceaa9d25b3a877b6311274eb6dc896b75/protobuf-6.31.1-cp39-cp39-win32.whl", hash = "sha256:0414e3aa5a5f3ff423828e1e6a6e907d6c65c1d5b7e6e975793d5590bdeecc16", size = 423626 }, + { url = "https://files.pythonhosted.org/packages/09/34/13989eb9f482409ed821bfa3e34e6a3878b42607c38e7f7572b4cc825091/protobuf-6.31.1-cp39-cp39-win_amd64.whl", hash = "sha256:8764cf4587791e7564051b35524b72844f845ad0bb011704c3736cce762d8fe9", size = 435347 }, + { url = "https://files.pythonhosted.org/packages/f7/af/ab3c51ab7507a7325e98ffe691d9495ee3d3aa5f589afad65ec920d39821/protobuf-6.31.1-py3-none-any.whl", hash = "sha256:720a6c7e6b77288b85063569baae8536671b39f15cc22037ec7045658d80489e", size = 168724 }, +] + +[[package]] +name = "pyarrow" +version = "20.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/ee/a7810cb9f3d6e9238e61d312076a9859bf3668fd21c69744de9532383912/pyarrow-20.0.0.tar.gz", hash = "sha256:febc4a913592573c8d5805091a6c2b5064c8bd6e002131f01061797d91c783c1", size = 1125187 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/23/77094eb8ee0dbe88441689cb6afc40ac312a1e15d3a7acc0586999518222/pyarrow-20.0.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:c7dd06fd7d7b410ca5dc839cc9d485d2bc4ae5240851bcd45d85105cc90a47d7", size = 30832591 }, + { url = "https://files.pythonhosted.org/packages/c3/d5/48cc573aff00d62913701d9fac478518f693b30c25f2c157550b0b2565cb/pyarrow-20.0.0-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:d5382de8dc34c943249b01c19110783d0d64b207167c728461add1ecc2db88e4", size = 32273686 }, + { url = "https://files.pythonhosted.org/packages/37/df/4099b69a432b5cb412dd18adc2629975544d656df3d7fda6d73c5dba935d/pyarrow-20.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6415a0d0174487456ddc9beaead703d0ded5966129fa4fd3114d76b5d1c5ceae", size = 41337051 }, + { url = "https://files.pythonhosted.org/packages/4c/27/99922a9ac1c9226f346e3a1e15e63dee6f623ed757ff2893f9d6994a69d3/pyarrow-20.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15aa1b3b2587e74328a730457068dc6c89e6dcbf438d4369f572af9d320a25ee", size = 42404659 }, + { url = "https://files.pythonhosted.org/packages/21/d1/71d91b2791b829c9e98f1e0d85be66ed93aff399f80abb99678511847eaa/pyarrow-20.0.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:5605919fbe67a7948c1f03b9f3727d82846c053cd2ce9303ace791855923fd20", size = 40695446 }, + { url = "https://files.pythonhosted.org/packages/f1/ca/ae10fba419a6e94329707487835ec721f5a95f3ac9168500bcf7aa3813c7/pyarrow-20.0.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:a5704f29a74b81673d266e5ec1fe376f060627c2e42c5c7651288ed4b0db29e9", size = 42278528 }, + { url = "https://files.pythonhosted.org/packages/7a/a6/aba40a2bf01b5d00cf9cd16d427a5da1fad0fb69b514ce8c8292ab80e968/pyarrow-20.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:00138f79ee1b5aca81e2bdedb91e3739b987245e11fa3c826f9e57c5d102fb75", size = 42918162 }, + { url = "https://files.pythonhosted.org/packages/93/6b/98b39650cd64f32bf2ec6d627a9bd24fcb3e4e6ea1873c5e1ea8a83b1a18/pyarrow-20.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f2d67ac28f57a362f1a2c1e6fa98bfe2f03230f7e15927aecd067433b1e70ce8", size = 44550319 }, + { url = "https://files.pythonhosted.org/packages/ab/32/340238be1eb5037e7b5de7e640ee22334417239bc347eadefaf8c373936d/pyarrow-20.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:4a8b029a07956b8d7bd742ffca25374dd3f634b35e46cc7a7c3fa4c75b297191", size = 25770759 }, + { url = "https://files.pythonhosted.org/packages/47/a2/b7930824181ceadd0c63c1042d01fa4ef63eee233934826a7a2a9af6e463/pyarrow-20.0.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:24ca380585444cb2a31324c546a9a56abbe87e26069189e14bdba19c86c049f0", size = 30856035 }, + { url = "https://files.pythonhosted.org/packages/9b/18/c765770227d7f5bdfa8a69f64b49194352325c66a5c3bb5e332dfd5867d9/pyarrow-20.0.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:95b330059ddfdc591a3225f2d272123be26c8fa76e8c9ee1a77aad507361cfdb", size = 32309552 }, + { url = "https://files.pythonhosted.org/packages/44/fb/dfb2dfdd3e488bb14f822d7335653092dde150cffc2da97de6e7500681f9/pyarrow-20.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f0fb1041267e9968c6d0d2ce3ff92e3928b243e2b6d11eeb84d9ac547308232", size = 41334704 }, + { url = "https://files.pythonhosted.org/packages/58/0d/08a95878d38808051a953e887332d4a76bc06c6ee04351918ee1155407eb/pyarrow-20.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8ff87cc837601532cc8242d2f7e09b4e02404de1b797aee747dd4ba4bd6313f", size = 42399836 }, + { url = "https://files.pythonhosted.org/packages/f3/cd/efa271234dfe38f0271561086eedcad7bc0f2ddd1efba423916ff0883684/pyarrow-20.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:7a3a5dcf54286e6141d5114522cf31dd67a9e7c9133d150799f30ee302a7a1ab", size = 40711789 }, + { url = "https://files.pythonhosted.org/packages/46/1f/7f02009bc7fc8955c391defee5348f510e589a020e4b40ca05edcb847854/pyarrow-20.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:a6ad3e7758ecf559900261a4df985662df54fb7fdb55e8e3b3aa99b23d526b62", size = 42301124 }, + { url = "https://files.pythonhosted.org/packages/4f/92/692c562be4504c262089e86757a9048739fe1acb4024f92d39615e7bab3f/pyarrow-20.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6bb830757103a6cb300a04610e08d9636f0cd223d32f388418ea893a3e655f1c", size = 42916060 }, + { url = "https://files.pythonhosted.org/packages/a4/ec/9f5c7e7c828d8e0a3c7ef50ee62eca38a7de2fa6eb1b8fa43685c9414fef/pyarrow-20.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:96e37f0766ecb4514a899d9a3554fadda770fb57ddf42b63d80f14bc20aa7db3", size = 44547640 }, + { url = "https://files.pythonhosted.org/packages/54/96/46613131b4727f10fd2ffa6d0d6f02efcc09a0e7374eff3b5771548aa95b/pyarrow-20.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:3346babb516f4b6fd790da99b98bed9708e3f02e734c84971faccb20736848dc", size = 25781491 }, + { url = "https://files.pythonhosted.org/packages/a1/d6/0c10e0d54f6c13eb464ee9b67a68b8c71bcf2f67760ef5b6fbcddd2ab05f/pyarrow-20.0.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:75a51a5b0eef32727a247707d4755322cb970be7e935172b6a3a9f9ae98404ba", size = 30815067 }, + { url = "https://files.pythonhosted.org/packages/7e/e2/04e9874abe4094a06fd8b0cbb0f1312d8dd7d707f144c2ec1e5e8f452ffa/pyarrow-20.0.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:211d5e84cecc640c7a3ab900f930aaff5cd2702177e0d562d426fb7c4f737781", size = 32297128 }, + { url = "https://files.pythonhosted.org/packages/31/fd/c565e5dcc906a3b471a83273039cb75cb79aad4a2d4a12f76cc5ae90a4b8/pyarrow-20.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ba3cf4182828be7a896cbd232aa8dd6a31bd1f9e32776cc3796c012855e1199", size = 41334890 }, + { url = "https://files.pythonhosted.org/packages/af/a9/3bdd799e2c9b20c1ea6dc6fa8e83f29480a97711cf806e823f808c2316ac/pyarrow-20.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c3a01f313ffe27ac4126f4c2e5ea0f36a5fc6ab51f8726cf41fee4b256680bd", size = 42421775 }, + { url = "https://files.pythonhosted.org/packages/10/f7/da98ccd86354c332f593218101ae56568d5dcedb460e342000bd89c49cc1/pyarrow-20.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:a2791f69ad72addd33510fec7bb14ee06c2a448e06b649e264c094c5b5f7ce28", size = 40687231 }, + { url = "https://files.pythonhosted.org/packages/bb/1b/2168d6050e52ff1e6cefc61d600723870bf569cbf41d13db939c8cf97a16/pyarrow-20.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:4250e28a22302ce8692d3a0e8ec9d9dde54ec00d237cff4dfa9c1fbf79e472a8", size = 42295639 }, + { url = "https://files.pythonhosted.org/packages/b2/66/2d976c0c7158fd25591c8ca55aee026e6d5745a021915a1835578707feb3/pyarrow-20.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:89e030dc58fc760e4010148e6ff164d2f44441490280ef1e97a542375e41058e", size = 42908549 }, + { url = "https://files.pythonhosted.org/packages/31/a9/dfb999c2fc6911201dcbf348247f9cc382a8990f9ab45c12eabfd7243a38/pyarrow-20.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6102b4864d77102dbbb72965618e204e550135a940c2534711d5ffa787df2a5a", size = 44557216 }, + { url = "https://files.pythonhosted.org/packages/a0/8e/9adee63dfa3911be2382fb4d92e4b2e7d82610f9d9f668493bebaa2af50f/pyarrow-20.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:96d6a0a37d9c98be08f5ed6a10831d88d52cac7b13f5287f1e0f625a0de8062b", size = 25660496 }, + { url = "https://files.pythonhosted.org/packages/9b/aa/daa413b81446d20d4dad2944110dcf4cf4f4179ef7f685dd5a6d7570dc8e/pyarrow-20.0.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:a15532e77b94c61efadde86d10957950392999503b3616b2ffcef7621a002893", size = 30798501 }, + { url = "https://files.pythonhosted.org/packages/ff/75/2303d1caa410925de902d32ac215dc80a7ce7dd8dfe95358c165f2adf107/pyarrow-20.0.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:dd43f58037443af715f34f1322c782ec463a3c8a94a85fdb2d987ceb5658e061", size = 32277895 }, + { url = "https://files.pythonhosted.org/packages/92/41/fe18c7c0b38b20811b73d1bdd54b1fccba0dab0e51d2048878042d84afa8/pyarrow-20.0.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa0d288143a8585806e3cc7c39566407aab646fb9ece164609dac1cfff45f6ae", size = 41327322 }, + { url = "https://files.pythonhosted.org/packages/da/ab/7dbf3d11db67c72dbf36ae63dcbc9f30b866c153b3a22ef728523943eee6/pyarrow-20.0.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b6953f0114f8d6f3d905d98e987d0924dabce59c3cda380bdfaa25a6201563b4", size = 42411441 }, + { url = "https://files.pythonhosted.org/packages/90/c3/0c7da7b6dac863af75b64e2f827e4742161128c350bfe7955b426484e226/pyarrow-20.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:991f85b48a8a5e839b2128590ce07611fae48a904cae6cab1f089c5955b57eb5", size = 40677027 }, + { url = "https://files.pythonhosted.org/packages/be/27/43a47fa0ff9053ab5203bb3faeec435d43c0d8bfa40179bfd076cdbd4e1c/pyarrow-20.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:97c8dc984ed09cb07d618d57d8d4b67a5100a30c3818c2fb0b04599f0da2de7b", size = 42281473 }, + { url = "https://files.pythonhosted.org/packages/bc/0b/d56c63b078876da81bbb9ba695a596eabee9b085555ed12bf6eb3b7cab0e/pyarrow-20.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9b71daf534f4745818f96c214dbc1e6124d7daf059167330b610fc69b6f3d3e3", size = 42893897 }, + { url = "https://files.pythonhosted.org/packages/92/ac/7d4bd020ba9145f354012838692d48300c1b8fe5634bfda886abcada67ed/pyarrow-20.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e8b88758f9303fa5a83d6c90e176714b2fd3852e776fc2d7e42a22dd6c2fb368", size = 44543847 }, + { url = "https://files.pythonhosted.org/packages/9d/07/290f4abf9ca702c5df7b47739c1b2c83588641ddfa2cc75e34a301d42e55/pyarrow-20.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:30b3051b7975801c1e1d387e17c588d8ab05ced9b1e14eec57915f79869b5031", size = 25653219 }, + { url = "https://files.pythonhosted.org/packages/95/df/720bb17704b10bd69dde086e1400b8eefb8f58df3f8ac9cff6c425bf57f1/pyarrow-20.0.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:ca151afa4f9b7bc45bcc791eb9a89e90a9eb2772767d0b1e5389609c7d03db63", size = 30853957 }, + { url = "https://files.pythonhosted.org/packages/d9/72/0d5f875efc31baef742ba55a00a25213a19ea64d7176e0fe001c5d8b6e9a/pyarrow-20.0.0-cp313-cp313t-macosx_12_0_x86_64.whl", hash = "sha256:4680f01ecd86e0dd63e39eb5cd59ef9ff24a9d166db328679e36c108dc993d4c", size = 32247972 }, + { url = "https://files.pythonhosted.org/packages/d5/bc/e48b4fa544d2eea72f7844180eb77f83f2030b84c8dad860f199f94307ed/pyarrow-20.0.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f4c8534e2ff059765647aa69b75d6543f9fef59e2cd4c6d18015192565d2b70", size = 41256434 }, + { url = "https://files.pythonhosted.org/packages/c3/01/974043a29874aa2cf4f87fb07fd108828fc7362300265a2a64a94965e35b/pyarrow-20.0.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e1f8a47f4b4ae4c69c4d702cfbdfe4d41e18e5c7ef6f1bb1c50918c1e81c57b", size = 42353648 }, + { url = "https://files.pythonhosted.org/packages/68/95/cc0d3634cde9ca69b0e51cbe830d8915ea32dda2157560dda27ff3b3337b/pyarrow-20.0.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:a1f60dc14658efaa927f8214734f6a01a806d7690be4b3232ba526836d216122", size = 40619853 }, + { url = "https://files.pythonhosted.org/packages/29/c2/3ad40e07e96a3e74e7ed7cc8285aadfa84eb848a798c98ec0ad009eb6bcc/pyarrow-20.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:204a846dca751428991346976b914d6d2a82ae5b8316a6ed99789ebf976551e6", size = 42241743 }, + { url = "https://files.pythonhosted.org/packages/eb/cb/65fa110b483339add6a9bc7b6373614166b14e20375d4daa73483755f830/pyarrow-20.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f3b117b922af5e4c6b9a9115825726cac7d8b1421c37c2b5e24fbacc8930612c", size = 42839441 }, + { url = "https://files.pythonhosted.org/packages/98/7b/f30b1954589243207d7a0fbc9997401044bf9a033eec78f6cb50da3f304a/pyarrow-20.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e724a3fd23ae5b9c010e7be857f4405ed5e679db5c93e66204db1a69f733936a", size = 44503279 }, + { url = "https://files.pythonhosted.org/packages/37/40/ad395740cd641869a13bcf60851296c89624662575621968dcfafabaa7f6/pyarrow-20.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:82f1ee5133bd8f49d31be1299dc07f585136679666b502540db854968576faf9", size = 25944982 }, + { url = "https://files.pythonhosted.org/packages/10/53/421820fa125138c868729b930d4bc487af2c4b01b1c6104818aab7e98f13/pyarrow-20.0.0-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:1bcbe471ef3349be7714261dea28fe280db574f9d0f77eeccc195a2d161fd861", size = 30844702 }, + { url = "https://files.pythonhosted.org/packages/2e/70/fd75e03312b715e90d928fb91ed8d45c9b0520346e5231b1c69293afd4c7/pyarrow-20.0.0-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:a18a14baef7d7ae49247e75641fd8bcbb39f44ed49a9fc4ec2f65d5031aa3b96", size = 32287180 }, + { url = "https://files.pythonhosted.org/packages/c4/e3/21e5758e46219fdedf5e6c800574dd9d17e962e80014cfe08d6d475be863/pyarrow-20.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb497649e505dc36542d0e68eca1a3c94ecbe9799cb67b578b55f2441a247fbc", size = 41351968 }, + { url = "https://files.pythonhosted.org/packages/ac/f5/ed6a4c4b11f9215092a35097a985485bb7d879cb79d93d203494e8604f4e/pyarrow-20.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11529a2283cb1f6271d7c23e4a8f9f8b7fd173f7360776b668e509d712a02eec", size = 42415208 }, + { url = "https://files.pythonhosted.org/packages/44/e5/466a63668ba25788ee8d38d55f853a60469ae7ad1cda343db9f3f45e0b0a/pyarrow-20.0.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:6fc1499ed3b4b57ee4e090e1cea6eb3584793fe3d1b4297bbf53f09b434991a5", size = 40708556 }, + { url = "https://files.pythonhosted.org/packages/e8/d7/4c4d4e4cf6e53e16a519366dfe9223ee4a7a38e6e28c1c0d372b38ba3fe7/pyarrow-20.0.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:db53390eaf8a4dab4dbd6d93c85c5cf002db24902dbff0ca7d988beb5c9dd15b", size = 42291754 }, + { url = "https://files.pythonhosted.org/packages/07/d5/79effb32585b7c18897d3047a2163034f3f9c944d12f7b2fd8df6a2edc70/pyarrow-20.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:851c6a8260ad387caf82d2bbf54759130534723e37083111d4ed481cb253cc0d", size = 42936483 }, + { url = "https://files.pythonhosted.org/packages/09/5c/f707603552c058b2e9129732de99a67befb1f13f008cc58856304a62c38b/pyarrow-20.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:e22f80b97a271f0a7d9cd07394a7d348f80d3ac63ed7cc38b6d1b696ab3b2619", size = 44558895 }, + { url = "https://files.pythonhosted.org/packages/26/cc/1eb6a01c1bbc787f596c270c46bcd2273e35154a84afcb1d0cb4cc72457e/pyarrow-20.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:9965a050048ab02409fb7cbbefeedba04d3d67f2cc899eff505cc084345959ca", size = 25785667 }, +] + +[[package]] +name = "pycparser" +version = "2.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 }, +] + +[[package]] +name = "pyparsing" +version = "3.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bb/22/f1129e69d94ffff626bdb5c835506b3a5b4f3d070f17ea295e12c2c6f60f/pyparsing-3.2.3.tar.gz", hash = "sha256:b9c13f1ab8b3b542f72e28f634bad4de758ab3ce4546e4301970ad6fa77c38be", size = 1088608 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/e7/df2285f3d08fee213f2d041540fa4fc9ca6c2d44cf36d3a035bf2a8d2bcc/pyparsing-3.2.3-py3-none-any.whl", hash = "sha256:a749938e02d6fd0b59b356ca504a24982314bb090c383e3cf201c95ef7e2bfcf", size = 111120 }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 }, +] + +[[package]] +name = "pyzmq" +version = "27.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "implementation_name == 'pypy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f1/06/50a4e9648b3e8b992bef8eb632e457307553a89d294103213cfd47b3da69/pyzmq-27.0.0.tar.gz", hash = "sha256:b1f08eeb9ce1510e6939b6e5dcd46a17765e2333daae78ecf4606808442e52cf", size = 280478 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/09/1681d4b047626d352c083770618ac29655ab1f5c20eee31dc94c000b9b7b/pyzmq-27.0.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:b973ee650e8f442ce482c1d99ca7ab537c69098d53a3d046676a484fd710c87a", size = 1329291 }, + { url = "https://files.pythonhosted.org/packages/9d/b2/9c9385225fdd54db9506ed8accbb9ea63ca813ba59d43d7f282a6a16a30b/pyzmq-27.0.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:661942bc7cd0223d569d808f2e5696d9cc120acc73bf3e88a1f1be7ab648a7e4", size = 905952 }, + { url = "https://files.pythonhosted.org/packages/41/73/333c72c7ec182cdffe25649e3da1c3b9f3cf1cede63cfdc23d1384d4a601/pyzmq-27.0.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:50360fb2a056ffd16e5f4177eee67f1dd1017332ea53fb095fe7b5bf29c70246", size = 666165 }, + { url = "https://files.pythonhosted.org/packages/a5/fe/fc7b9c1a50981928e25635a926653cb755364316db59ccd6e79cfb9a0b4f/pyzmq-27.0.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf209a6dc4b420ed32a7093642843cbf8703ed0a7d86c16c0b98af46762ebefb", size = 853755 }, + { url = "https://files.pythonhosted.org/packages/8c/4c/740ed4b6e8fa160cd19dc5abec8db68f440564b2d5b79c1d697d9862a2f7/pyzmq-27.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c2dace4a7041cca2fba5357a2d7c97c5effdf52f63a1ef252cfa496875a3762d", size = 1654868 }, + { url = "https://files.pythonhosted.org/packages/97/00/875b2ecfcfc78ab962a59bd384995186818524ea957dc8ad3144611fae12/pyzmq-27.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:63af72b2955fc77caf0a77444baa2431fcabb4370219da38e1a9f8d12aaebe28", size = 2033443 }, + { url = "https://files.pythonhosted.org/packages/60/55/6dd9c470c42d713297c5f2a56f7903dc1ebdb4ab2edda996445c21651900/pyzmq-27.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e8c4adce8e37e75c4215297d7745551b8dcfa5f728f23ce09bf4e678a9399413", size = 1891288 }, + { url = "https://files.pythonhosted.org/packages/28/5d/54b0ef50d40d7c65a627f4a4b4127024ba9820f2af8acd933a4d30ae192e/pyzmq-27.0.0-cp310-cp310-win32.whl", hash = "sha256:5d5ef4718ecab24f785794e0e7536436698b459bfbc19a1650ef55280119d93b", size = 567936 }, + { url = "https://files.pythonhosted.org/packages/18/ea/dedca4321de748ca48d3bcdb72274d4d54e8d84ea49088d3de174bd45d88/pyzmq-27.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:e40609380480b3d12c30f841323f42451c755b8fece84235236f5fe5ffca8c1c", size = 628686 }, + { url = "https://files.pythonhosted.org/packages/d4/a7/fcdeedc306e71e94ac262cba2d02337d885f5cdb7e8efced8e5ffe327808/pyzmq-27.0.0-cp310-cp310-win_arm64.whl", hash = "sha256:6b0397b0be277b46762956f576e04dc06ced265759e8c2ff41a0ee1aa0064198", size = 559039 }, + { url = "https://files.pythonhosted.org/packages/44/df/84c630654106d9bd9339cdb564aa941ed41b023a0264251d6743766bb50e/pyzmq-27.0.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:21457825249b2a53834fa969c69713f8b5a79583689387a5e7aed880963ac564", size = 1332718 }, + { url = "https://files.pythonhosted.org/packages/c1/8e/f6a5461a07654d9840d256476434ae0ff08340bba562a455f231969772cb/pyzmq-27.0.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1958947983fef513e6e98eff9cb487b60bf14f588dc0e6bf35fa13751d2c8251", size = 908248 }, + { url = "https://files.pythonhosted.org/packages/7c/93/82863e8d695a9a3ae424b63662733ae204a295a2627d52af2f62c2cd8af9/pyzmq-27.0.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0dc628b5493f9a8cd9844b8bee9732ef587ab00002157c9329e4fc0ef4d3afa", size = 668647 }, + { url = "https://files.pythonhosted.org/packages/f3/85/15278769b348121eacdbfcbd8c4d40f1102f32fa6af5be1ffc032ed684be/pyzmq-27.0.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7bbe9e1ed2c8d3da736a15694d87c12493e54cc9dc9790796f0321794bbc91f", size = 856600 }, + { url = "https://files.pythonhosted.org/packages/d4/af/1c469b3d479bd095edb28e27f12eee10b8f00b356acbefa6aeb14dd295d1/pyzmq-27.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dc1091f59143b471d19eb64f54bae4f54bcf2a466ffb66fe45d94d8d734eb495", size = 1657748 }, + { url = "https://files.pythonhosted.org/packages/8c/f4/17f965d0ee6380b1d6326da842a50e4b8b9699745161207945f3745e8cb5/pyzmq-27.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7011ade88c8e535cf140f8d1a59428676fbbce7c6e54fefce58bf117aefb6667", size = 2034311 }, + { url = "https://files.pythonhosted.org/packages/e0/6e/7c391d81fa3149fd759de45d298003de6cfab343fb03e92c099821c448db/pyzmq-27.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2c386339d7e3f064213aede5d03d054b237937fbca6dd2197ac8cf3b25a6b14e", size = 1893630 }, + { url = "https://files.pythonhosted.org/packages/0e/e0/eaffe7a86f60e556399e224229e7769b717f72fec0706b70ab2c03aa04cb/pyzmq-27.0.0-cp311-cp311-win32.whl", hash = "sha256:0546a720c1f407b2172cb04b6b094a78773491497e3644863cf5c96c42df8cff", size = 567706 }, + { url = "https://files.pythonhosted.org/packages/c9/05/89354a8cffdcce6e547d48adaaf7be17007fc75572123ff4ca90a4ca04fc/pyzmq-27.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:15f39d50bd6c9091c67315ceb878a4f531957b121d2a05ebd077eb35ddc5efed", size = 630322 }, + { url = "https://files.pythonhosted.org/packages/fa/07/4ab976d5e1e63976719389cc4f3bfd248a7f5f2bb2ebe727542363c61b5f/pyzmq-27.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c5817641eebb391a2268c27fecd4162448e03538387093cdbd8bf3510c316b38", size = 558435 }, + { url = "https://files.pythonhosted.org/packages/93/a7/9ad68f55b8834ede477842214feba6a4c786d936c022a67625497aacf61d/pyzmq-27.0.0-cp312-abi3-macosx_10_15_universal2.whl", hash = "sha256:cbabc59dcfaac66655c040dfcb8118f133fb5dde185e5fc152628354c1598e52", size = 1305438 }, + { url = "https://files.pythonhosted.org/packages/ba/ee/26aa0f98665a22bc90ebe12dced1de5f3eaca05363b717f6fb229b3421b3/pyzmq-27.0.0-cp312-abi3-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:cb0ac5179cba4b2f94f1aa208fbb77b62c4c9bf24dd446278b8b602cf85fcda3", size = 895095 }, + { url = "https://files.pythonhosted.org/packages/cf/85/c57e7ab216ecd8aa4cc7e3b83b06cc4e9cf45c87b0afc095f10cd5ce87c1/pyzmq-27.0.0-cp312-abi3-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53a48f0228eab6cbf69fde3aa3c03cbe04e50e623ef92ae395fce47ef8a76152", size = 651826 }, + { url = "https://files.pythonhosted.org/packages/69/9a/9ea7e230feda9400fb0ae0d61d7d6ddda635e718d941c44eeab22a179d34/pyzmq-27.0.0-cp312-abi3-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:111db5f395e09f7e775f759d598f43cb815fc58e0147623c4816486e1a39dc22", size = 839750 }, + { url = "https://files.pythonhosted.org/packages/08/66/4cebfbe71f3dfbd417011daca267539f62ed0fbc68105357b68bbb1a25b7/pyzmq-27.0.0-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c8878011653dcdc27cc2c57e04ff96f0471e797f5c19ac3d7813a245bcb24371", size = 1641357 }, + { url = "https://files.pythonhosted.org/packages/ac/f6/b0f62578c08d2471c791287149cb8c2aaea414ae98c6e995c7dbe008adfb/pyzmq-27.0.0-cp312-abi3-musllinux_1_2_i686.whl", hash = "sha256:c0ed2c1f335ba55b5fdc964622254917d6b782311c50e138863eda409fbb3b6d", size = 2020281 }, + { url = "https://files.pythonhosted.org/packages/37/b9/4f670b15c7498495da9159edc374ec09c88a86d9cd5a47d892f69df23450/pyzmq-27.0.0-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e918d70862d4cfd4b1c187310015646a14e1f5917922ab45b29f28f345eeb6be", size = 1877110 }, + { url = "https://files.pythonhosted.org/packages/66/31/9dee25c226295b740609f0d46db2fe972b23b6f5cf786360980524a3ba92/pyzmq-27.0.0-cp312-abi3-win32.whl", hash = "sha256:88b4e43cab04c3c0f0d55df3b1eef62df2b629a1a369b5289a58f6fa8b07c4f4", size = 559297 }, + { url = "https://files.pythonhosted.org/packages/9b/12/52da5509800f7ff2d287b2f2b4e636e7ea0f001181cba6964ff6c1537778/pyzmq-27.0.0-cp312-abi3-win_amd64.whl", hash = "sha256:dce4199bf5f648a902ce37e7b3afa286f305cd2ef7a8b6ec907470ccb6c8b371", size = 619203 }, + { url = "https://files.pythonhosted.org/packages/93/6d/7f2e53b19d1edb1eb4f09ec7c3a1f945ca0aac272099eab757d15699202b/pyzmq-27.0.0-cp312-abi3-win_arm64.whl", hash = "sha256:56e46bbb85d52c1072b3f809cc1ce77251d560bc036d3a312b96db1afe76db2e", size = 551927 }, + { url = "https://files.pythonhosted.org/packages/19/62/876b27c4ff777db4ceba1c69ea90d3c825bb4f8d5e7cd987ce5802e33c55/pyzmq-27.0.0-cp313-cp313t-macosx_10_15_universal2.whl", hash = "sha256:c36ad534c0c29b4afa088dc53543c525b23c0797e01b69fef59b1a9c0e38b688", size = 1340826 }, + { url = "https://files.pythonhosted.org/packages/43/69/58ef8f4f59d3bcd505260c73bee87b008850f45edca40ddaba54273c35f4/pyzmq-27.0.0-cp313-cp313t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:67855c14173aec36395d7777aaba3cc527b393821f30143fd20b98e1ff31fd38", size = 897283 }, + { url = "https://files.pythonhosted.org/packages/43/15/93a0d0396700a60475ad3c5d42c5f1c308d3570bc94626b86c71ef9953e0/pyzmq-27.0.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8617c7d43cd8ccdb62aebe984bfed77ca8f036e6c3e46dd3dddda64b10f0ab7a", size = 660567 }, + { url = "https://files.pythonhosted.org/packages/0e/b3/fe055513e498ca32f64509abae19b9c9eb4d7c829e02bd8997dd51b029eb/pyzmq-27.0.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:67bfbcbd0a04c575e8103a6061d03e393d9f80ffdb9beb3189261e9e9bc5d5e9", size = 847681 }, + { url = "https://files.pythonhosted.org/packages/b6/4f/ff15300b00b5b602191f3df06bbc8dd4164e805fdd65bb77ffbb9c5facdc/pyzmq-27.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5cd11d46d7b7e5958121b3eaf4cd8638eff3a720ec527692132f05a57f14341d", size = 1650148 }, + { url = "https://files.pythonhosted.org/packages/c4/6f/84bdfff2a224a6f26a24249a342e5906993c50b0761e311e81b39aef52a7/pyzmq-27.0.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:b801c2e40c5aa6072c2f4876de8dccd100af6d9918d4d0d7aa54a1d982fd4f44", size = 2023768 }, + { url = "https://files.pythonhosted.org/packages/64/39/dc2db178c26a42228c5ac94a9cc595030458aa64c8d796a7727947afbf55/pyzmq-27.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:20d5cb29e8c5f76a127c75b6e7a77e846bc4b655c373baa098c26a61b7ecd0ef", size = 1885199 }, + { url = "https://files.pythonhosted.org/packages/c7/21/dae7b06a1f8cdee5d8e7a63d99c5d129c401acc40410bef2cbf42025e26f/pyzmq-27.0.0-cp313-cp313t-win32.whl", hash = "sha256:a20528da85c7ac7a19b7384e8c3f8fa707841fd85afc4ed56eda59d93e3d98ad", size = 575439 }, + { url = "https://files.pythonhosted.org/packages/eb/bc/1709dc55f0970cf4cb8259e435e6773f9946f41a045c2cb90e870b7072da/pyzmq-27.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:d8229f2efece6a660ee211d74d91dbc2a76b95544d46c74c615e491900dc107f", size = 639933 }, + { url = "https://files.pythonhosted.org/packages/19/dc/95210fe17e5d7dba89bd663e1d88f50a8003f296284731b09f1d95309a42/pyzmq-27.0.0-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:100f6e5052ba42b2533011d34a018a5ace34f8cac67cb03cfa37c8bdae0ca617", size = 1330656 }, + { url = "https://files.pythonhosted.org/packages/d3/7e/63f742b578316258e03ecb393d35c0964348d80834bdec8a100ed7bb9c91/pyzmq-27.0.0-cp39-cp39-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:bf6c6b061efd00404b9750e2cfbd9507492c8d4b3721ded76cb03786131be2ed", size = 906522 }, + { url = "https://files.pythonhosted.org/packages/1f/bf/f0b2b67f5a9bfe0fbd0e978a2becd901f802306aa8e29161cb0963094352/pyzmq-27.0.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ee05728c0b0b2484a9fc20466fa776fffb65d95f7317a3419985b8c908563861", size = 863545 }, + { url = "https://files.pythonhosted.org/packages/87/0e/7d90ccd2ef577c8bae7f926acd2011a6d960eea8a068c5fd52b419206960/pyzmq-27.0.0-cp39-cp39-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7cdf07fe0a557b131366f80727ec8ccc4b70d89f1e3f920d94a594d598d754f0", size = 666796 }, + { url = "https://files.pythonhosted.org/packages/4f/6d/ca8007a313baa73361778773aef210f4902e68f468d1f93b6c8b908fabbd/pyzmq-27.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:90252fa2ff3a104219db1f5ced7032a7b5fc82d7c8d2fec2b9a3e6fd4e25576b", size = 1655599 }, + { url = "https://files.pythonhosted.org/packages/46/de/5cb4f99d6c0dd8f33d729c9ebd49af279586e5ab127e93aa6ef0ecd08c4c/pyzmq-27.0.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:ea6d441c513bf18c578c73c323acf7b4184507fc244762193aa3a871333c9045", size = 2034119 }, + { url = "https://files.pythonhosted.org/packages/d0/8d/57cc90c8b5f30a97a7e86ec91a3b9822ec7859d477e9c30f531fb78f4a97/pyzmq-27.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:ae2b34bcfaae20c064948a4113bf8709eee89fd08317eb293ae4ebd69b4d9740", size = 1891955 }, + { url = "https://files.pythonhosted.org/packages/24/f5/a7012022573188903802ab75b5314b00e5c629228f3a36fadb421a42ebff/pyzmq-27.0.0-cp39-cp39-win32.whl", hash = "sha256:5b10bd6f008937705cf6e7bf8b6ece5ca055991e3eb130bca8023e20b86aa9a3", size = 568497 }, + { url = "https://files.pythonhosted.org/packages/9b/f3/2a4b2798275a574801221d94d599ed3e26d19f6378a7364cdfa3bee53944/pyzmq-27.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:00387d12a8af4b24883895f7e6b9495dc20a66027b696536edac35cb988c38f3", size = 629315 }, + { url = "https://files.pythonhosted.org/packages/da/eb/386a70314f305816142d6e8537f5557e5fd9614c03698d6c88cbd6c41190/pyzmq-27.0.0-cp39-cp39-win_arm64.whl", hash = "sha256:4c19d39c04c29a6619adfeb19e3735c421b3bfee082f320662f52e59c47202ba", size = 559596 }, + { url = "https://files.pythonhosted.org/packages/09/6f/be6523a7f3821c0b5370912ef02822c028611360e0d206dd945bdbf9eaef/pyzmq-27.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:656c1866505a5735d0660b7da6d7147174bbf59d4975fc2b7f09f43c9bc25745", size = 835950 }, + { url = "https://files.pythonhosted.org/packages/c6/1e/a50fdd5c15018de07ab82a61bc460841be967ee7bbe7abee3b714d66f7ac/pyzmq-27.0.0-pp310-pypy310_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:74175b9e12779382432dd1d1f5960ebe7465d36649b98a06c6b26be24d173fab", size = 799876 }, + { url = "https://files.pythonhosted.org/packages/88/a1/89eb5b71f5a504f8f887aceb8e1eb3626e00c00aa8085381cdff475440dc/pyzmq-27.0.0-pp310-pypy310_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8c6de908465697a8708e4d6843a1e884f567962fc61eb1706856545141d0cbb", size = 567400 }, + { url = "https://files.pythonhosted.org/packages/56/aa/4571dbcff56cfb034bac73fde8294e123c975ce3eea89aff31bf6dc6382b/pyzmq-27.0.0-pp310-pypy310_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c644aaacc01d0df5c7072826df45e67301f191c55f68d7b2916d83a9ddc1b551", size = 747031 }, + { url = "https://files.pythonhosted.org/packages/46/e0/d25f30fe0991293c5b2f5ef3b070d35fa6d57c0c7428898c3ab4913d0297/pyzmq-27.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:10f70c1d9a446a85013a36871a296007f6fe4232b530aa254baf9da3f8328bc0", size = 544726 }, + { url = "https://files.pythonhosted.org/packages/98/a6/92394373b8dbc1edc9d53c951e8d3989d518185174ee54492ec27711779d/pyzmq-27.0.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cd1dc59763effd1576f8368047c9c31468fce0af89d76b5067641137506792ae", size = 835948 }, + { url = "https://files.pythonhosted.org/packages/56/f3/4dc38d75d9995bfc18773df3e41f2a2ca9b740b06f1a15dbf404077e7588/pyzmq-27.0.0-pp311-pypy311_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:60e8cc82d968174650c1860d7b716366caab9973787a1c060cf8043130f7d0f7", size = 799874 }, + { url = "https://files.pythonhosted.org/packages/ab/ba/64af397e0f421453dc68e31d5e0784d554bf39013a2de0872056e96e58af/pyzmq-27.0.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:14fe7aaac86e4e93ea779a821967360c781d7ac5115b3f1a171ced77065a0174", size = 567400 }, + { url = "https://files.pythonhosted.org/packages/63/87/ec956cbe98809270b59a22891d5758edae147a258e658bf3024a8254c855/pyzmq-27.0.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6ad0562d4e6abb785be3e4dd68599c41be821b521da38c402bc9ab2a8e7ebc7e", size = 747031 }, + { url = "https://files.pythonhosted.org/packages/be/8a/4a3764a68abc02e2fbb0668d225b6fda5cd39586dd099cee8b2ed6ab0452/pyzmq-27.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:9df43a2459cd3a3563404c1456b2c4c69564daa7dbaf15724c09821a3329ce46", size = 544726 }, + { url = "https://files.pythonhosted.org/packages/03/f6/11b2a6c8cd13275c31cddc3f89981a1b799a3c41dec55289fa18dede96b5/pyzmq-27.0.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:39ddd3ba0a641f01d8f13a3cfd4c4924eb58e660d8afe87e9061d6e8ca6f7ac3", size = 835944 }, + { url = "https://files.pythonhosted.org/packages/73/34/aa39076f4e07ae1912fa4b966fe24e831e01d736d4c1c7e8a3aa28a555b5/pyzmq-27.0.0-pp39-pypy39_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:8ca7e6a0388dd9e1180b14728051068f4efe83e0d2de058b5ff92c63f399a73f", size = 799869 }, + { url = "https://files.pythonhosted.org/packages/65/f3/81ed6b3dd242408ee79c0d8a88734644acf208baee8666ecd7e52664cf55/pyzmq-27.0.0-pp39-pypy39_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2524c40891be6a3106885a3935d58452dd83eb7a5742a33cc780a1ad4c49dec0", size = 758371 }, + { url = "https://files.pythonhosted.org/packages/e1/04/dac4ca674764281caf744e8adefd88f7e325e1605aba0f9a322094b903fa/pyzmq-27.0.0-pp39-pypy39_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6a56e3e5bd2d62a01744fd2f1ce21d760c7c65f030e9522738d75932a14ab62a", size = 567393 }, + { url = "https://files.pythonhosted.org/packages/51/8b/619a9ee2fa4d3c724fbadde946427735ade64da03894b071bbdc3b789d83/pyzmq-27.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:096af9e133fec3a72108ddefba1e42985cb3639e9de52cfd336b6fc23aa083e9", size = 544715 }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 }, +] + +[[package]] +name = "tomli" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077 }, + { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429 }, + { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067 }, + { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030 }, + { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898 }, + { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894 }, + { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319 }, + { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273 }, + { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310 }, + { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309 }, + { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762 }, + { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453 }, + { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486 }, + { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349 }, + { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159 }, + { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243 }, + { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645 }, + { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584 }, + { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875 }, + { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418 }, + { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708 }, + { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582 }, + { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543 }, + { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691 }, + { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170 }, + { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530 }, + { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666 }, + { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954 }, + { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724 }, + { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383 }, + { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 }, +] + +[[package]] +name = "types-protobuf" +version = "6.30.2.20250703" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/54/d63ce1eee8e93c4d710bbe2c663ec68e3672cf4f2fca26eecd20981c0c5d/types_protobuf-6.30.2.20250703.tar.gz", hash = "sha256:609a974754bbb71fa178fc641f51050395e8e1849f49d0420a6281ed8d1ddf46", size = 62300 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/2b/5d0377c3d6e0f49d4847ad2c40629593fee4a5c9ec56eba26a15c708fbc0/types_protobuf-6.30.2.20250703-py3-none-any.whl", hash = "sha256:fa5aff9036e9ef432d703abbdd801b436a249b6802e4df5ef74513e272434e57", size = 76489 }, +] + +[[package]] +name = "typing-extensions" +version = "4.14.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906 }, +] + +[[package]] +name = "zipp" +version = "3.23.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276 }, +] From 118c154b6fe9aa0aaedf997b8b19785f084309d3 Mon Sep 17 00:00:00 2001 From: CameleoGrey Date: Sat, 19 Jul 2025 22:36:45 +0300 Subject: [PATCH 2/4] v0.3.4 State management bug fix. Now it's possible to use greynet for truly incremental score calculation! (TODO: reimplement greynet in Rust) --- examples/greynet/greynet_example_core.py | 2 +- .../persistence/CotwinBuilderNQueens.py | 6 ++-- .../solve_nqueens_greynet_experimental.py | 6 ++-- greyjack/Cargo.lock | 2 +- greyjack/Cargo.toml | 2 +- greyjack/greyjack/agents/base/Agent.py | 21 +++--------- .../greynet/nodes/beta_node.py | 34 +++++++++++++++++-- .../score_requesters/OOPScoreRequester.py | 3 -- greyjack/pyproject.toml | 2 +- .../concrete_late_acceptance_macros.rs | 14 ++++---- .../concrete_simulated_annealing_macros.rs | 16 ++++----- greyjack/uv.lock | 2 +- 12 files changed, 61 insertions(+), 49 deletions(-) diff --git a/examples/greynet/greynet_example_core.py b/examples/greynet/greynet_example_core.py index 5343447..5bafa78 100644 --- a/examples/greynet/greynet_example_core.py +++ b/examples/greynet/greynet_example_core.py @@ -122,7 +122,7 @@ def max_consecutive_days(): ) .join( builder.for_each(CompanyPolicy), - JoinerType.GREATER_THAN, # A dummy join to bring the policy into the stream + JoinerType.GREATER_THAN, left_key_func=lambda name, sequences: 1, right_key_func=lambda policy: 0 ) diff --git a/examples/object_oriented/nqueens/persistence/CotwinBuilderNQueens.py b/examples/object_oriented/nqueens/persistence/CotwinBuilderNQueens.py index e6ecc1f..84d2098 100644 --- a/examples/object_oriented/nqueens/persistence/CotwinBuilderNQueens.py +++ b/examples/object_oriented/nqueens/persistence/CotwinBuilderNQueens.py @@ -26,7 +26,6 @@ def build_cotwin(self, domain_model, is_already_initialized): column_id = i planning_row_id = GJInteger(0, n-1, False, queens[i].row.row_id, None) cot_queen = CotQueen( queen_id, planning_row_id, column_id ) - cot_queen.greynet_fact_id = queen_id cot_queens.append( cot_queen ) nqueens_cotwin = NQueensCotwin() @@ -35,7 +34,10 @@ def build_cotwin(self, domain_model, is_already_initialized): nqueens_cotwin.set_score_calculator( PlainScoreCalculatorNQueens() ) elif self.scorer_name == "pseudo": nqueens_cotwin.set_score_calculator( IncrementalScoreCalculatorNQueens() ) - elif self.scorer_name == "greynet": + elif self.scorer_name == "greynet_plain": + greynet_score_calculator_nqueens.is_incremental = False + nqueens_cotwin.set_score_calculator( greynet_score_calculator_nqueens ) + elif self.scorer_name == "greynet_incremental": nqueens_cotwin.set_score_calculator( greynet_score_calculator_nqueens ) else: raise ValueError("Available score calculators: plain, pseudo, greynet") diff --git a/examples/object_oriented/nqueens/scripts/solve_nqueens_greynet_experimental.py b/examples/object_oriented/nqueens/scripts/solve_nqueens_greynet_experimental.py index 9e43b07..ef48c3a 100644 --- a/examples/object_oriented/nqueens/scripts/solve_nqueens_greynet_experimental.py +++ b/examples/object_oriented/nqueens/scripts/solve_nqueens_greynet_experimental.py @@ -22,8 +22,8 @@ try: # build domain model - domain_builder = DomainBuilderNQueens(8, random_seed=45) - cotwin_builder = CotwinBuilderNQueens(scorer_name="greynet") + domain_builder = DomainBuilderNQueens(16, random_seed=45) + cotwin_builder = CotwinBuilderNQueens(scorer_name="greynet_incremental") #domain = domain_builder.build_domain_from_scratch() #print(domain) @@ -32,7 +32,7 @@ #termination_strategy = TimeSpentLimit(time_seconds_limit=60) #termination_strategy = ScoreNoImprovement(time_seconds_limit=15) termination_strategy = ScoreLimit(score_to_compare=[0]) - agent = TabuSearch(neighbours_count=1, tabu_entity_rate=0.0, + agent = TabuSearch(neighbours_count=20, tabu_entity_rate=0.0, mutation_rate_multiplier=None, move_probas=[0, 1, 0, 0, 0, 0], migration_frequency=9999999999999, termination_strategy=termination_strategy) """agent = GeneticAlgorithm(population_size=128, crossover_probability=0.5, p_best_rate=0.05, diff --git a/greyjack/Cargo.lock b/greyjack/Cargo.lock index 2d4e0a1..4d1bbb1 100644 --- a/greyjack/Cargo.lock +++ b/greyjack/Cargo.lock @@ -579,7 +579,7 @@ checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" [[package]] name = "greyjack" -version = "0.3.3" +version = "0.3.4" dependencies = [ "chrono", "ndarray", diff --git a/greyjack/Cargo.toml b/greyjack/Cargo.toml index 22ba600..1861ea4 100644 --- a/greyjack/Cargo.toml +++ b/greyjack/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "greyjack" -version = "0.3.3" +version = "0.3.4" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/greyjack/greyjack/agents/base/Agent.py b/greyjack/greyjack/agents/base/Agent.py index 2c2da53..1f84ffe 100644 --- a/greyjack/greyjack/agents/base/Agent.py +++ b/greyjack/greyjack/agents/base/Agent.py @@ -229,18 +229,6 @@ def _init_population(self): scores = self.score_requester.request_score_incremental(generated_sample, deltas) self.population.append(self.individual_type(generated_sample, scores[0])) - #if self.score_requester.is_greynet: - - ################## - # TODO: understand, why produces incorrect results - #self.score_requester.cotwin.score_calculator.commit_deltas(deltas[0]) - - #self.score_requester.cotwin.score_calculator._apply_deltas_internal(deltas[0]) - #self.score_requester.cotwin.score_calculator.update_entity_mapping_incremental(deltas[0]) - #new_score = self.score_requester.cotwin.score_calculator.get_score() - #self.population[0] = self.individual_type(self.population[0].variable_values, new_score) - ################## - def _step_plain(self): new_population = [] @@ -270,8 +258,7 @@ def _step_incremental(self): if self.score_requester.is_greynet and new_values is not None: ################## - # TODO: understand, why produces incorrect results - #self.score_requester.cotwin.score_calculator.commit_deltas(new_values) + self.score_requester.cotwin.score_calculator.commit_deltas(new_values) #self.score_requester.cotwin.score_calculator._apply_deltas_internal(new_values) #self.score_requester.cotwin.score_calculator.update_entity_mapping_incremental(new_values) @@ -281,9 +268,9 @@ def _step_incremental(self): ################## # gives correct results, but lacks of performance due to linear updates for each acceptable solution - self.score_requester.cotwin.score_calculator._apply_deltas_internal(list(enumerate(new_population[0].variable_values))) - new_score = self.score_requester.cotwin.score_calculator.get_score() - new_population[0] = self.individual_type(new_population[0].variable_values, new_score) + #self.score_requester.cotwin.score_calculator._apply_deltas_internal(list(enumerate(new_population[0].variable_values))) + #new_score = self.score_requester.cotwin.score_calculator.get_score() + #new_population[0] = self.individual_type(new_population[0].variable_values, new_score) ################## #new_score = self.score_requester.cotwin.score_calculator._full_sync_and_get_score(new_population[0].variable_values) diff --git a/greyjack/greyjack/score_calculation/greynet/nodes/beta_node.py b/greyjack/greyjack/score_calculation/greynet/nodes/beta_node.py index 4e76dc9..12d588b 100644 --- a/greyjack/greyjack/score_calculation/greynet/nodes/beta_node.py +++ b/greyjack/greyjack/score_calculation/greynet/nodes/beta_node.py @@ -7,6 +7,23 @@ from ..core.tuple import TupleState, AbstractTuple from ..common.joiner_type import JoinerType +# --- Start of Bug Fix --- +# Defines the logical inverse for each joiner type, which is essential for +# creating symmetrical join logic within the BetaNode. +JOINER_INVERSES = { + JoinerType.EQUAL: JoinerType.EQUAL, + JoinerType.NOT_EQUAL: JoinerType.NOT_EQUAL, + JoinerType.LESS_THAN: JoinerType.GREATER_THAN, + JoinerType.LESS_THAN_OR_EQUAL: JoinerType.GREATER_THAN_OR_EQUAL, + JoinerType.GREATER_THAN: JoinerType.LESS_THAN, + JoinerType.GREATER_THAN_OR_EQUAL: JoinerType.LESS_THAN_OR_EQUAL, + JoinerType.RANGE_OVERLAPS: JoinerType.RANGE_OVERLAPS, + JoinerType.RANGE_CONTAINS: JoinerType.RANGE_WITHIN, + JoinerType.RANGE_WITHIN: JoinerType.RANGE_CONTAINS, +} +# --- End of Bug Fix --- + + class BetaNode(AbstractNode, ABC): """ An abstract base class for all join nodes (Bi, Tri, Quad, etc.). @@ -17,8 +34,22 @@ def __init__(self, node_id, joiner_type, left_index_properties, right_index_prop self.scheduler = scheduler self.tuple_pool = tuple_pool self.joiner_type = joiner_type + + # --- Start of Bug Fix --- + # The join is defined from left to right (e.g., left.key < right.key). + # When a new right_tuple arrives, we probe the left_index to find left_tuples + # that satisfy the original joiner (e.g., left.key < new_right.key). self.left_index = self._create_index(left_index_properties, joiner_type) - self.right_index = self._create_index(right_index_properties, joiner_type) + + # When a new left_tuple arrives, we must probe the right_index using the + # inverse joiner to find right_tuples that satisfy the condition. + # e.g., to find 'right' where 'new_left.key < right.key', we must query for + # 'right.key > new_left.key'. + inverse_joiner = JOINER_INVERSES.get(joiner_type) + if inverse_joiner is None: + raise ValueError(f"Joiner type {joiner_type} has no defined inverse.") + self.right_index = self._create_index(right_index_properties, inverse_joiner) + # --- End of Bug Fix --- self.beta_memory = {} @@ -84,4 +115,3 @@ def insert(self, tuple_): def retract(self, tuple_): raise NotImplementedError("BetaNode requires directional retract via an adapter.") - diff --git a/greyjack/greyjack/score_calculation/score_requesters/OOPScoreRequester.py b/greyjack/greyjack/score_calculation/score_requesters/OOPScoreRequester.py index 8a91889..1343eef 100644 --- a/greyjack/greyjack/score_calculation/score_requesters/OOPScoreRequester.py +++ b/greyjack/greyjack/score_calculation/score_requesters/OOPScoreRequester.py @@ -36,15 +36,12 @@ def _init_greynet(self): # Build the crucial mapping from solver's variable index to the domain object var_idx_to_entity_map = {} i = 0 - greynet_fact_id = 0 for group_name in self.cotwin.planning_entities: for native_entity, initialized_entity in zip(self.cotwin.planning_entities[group_name], initialized_planning_entities[group_name]): for attr_name, attr_value in native_entity.__dict__.items(): if type(attr_value) in {GJFloat, GJInteger, GJBinary}: var_idx_to_entity_map[i] = (initialized_entity, attr_name) i += 1 - initialized_entity.greynet_fact_id = greynet_fact_id - greynet_fact_id += 1 calculator.var_idx_to_entity_map = var_idx_to_entity_map except: print(traceback.format_exc()) diff --git a/greyjack/pyproject.toml b/greyjack/pyproject.toml index 1b3bab5..3784797 100644 --- a/greyjack/pyproject.toml +++ b/greyjack/pyproject.toml @@ -7,7 +7,7 @@ features = ["pyo3/extension-module"] [project] name = "greyjack" -version = "0.3.3" +version = "0.3.4" requires-python = ">=3.9" dependencies = [ "bitarray>=3.5.0", diff --git a/greyjack/src/agents/base/metaheuristic_bases/concrete_late_acceptance_macros.rs b/greyjack/src/agents/base/metaheuristic_bases/concrete_late_acceptance_macros.rs index a55edaa..0842d53 100644 --- a/greyjack/src/agents/base/metaheuristic_bases/concrete_late_acceptance_macros.rs +++ b/greyjack/src/agents/base/metaheuristic_bases/concrete_late_acceptance_macros.rs @@ -149,7 +149,7 @@ macro_rules! build_concrete_late_acceptance_base { sample: Vec, deltas: Vec>, scores: Vec<$score_type>, - ) -> Vec<$individual_variant> { + ) -> (Vec<$individual_variant>, Option>) { let late_native_score; if self.late_scores.len() == 0 { @@ -161,23 +161,21 @@ macro_rules! build_concrete_late_acceptance_base { let candidate_score = scores[0].clone(); let mut sample = sample; - let new_population:Vec<$individual_variant>; if (candidate_score <= late_native_score) || (candidate_score <= current_population[0].score) { - let best_deltas = &deltas[0]; - for (var_id, new_value) in best_deltas { + let best_deltas = deltas[0].clone(); + for (var_id, new_value) in &best_deltas { sample[*var_id] = *new_value; } let best_candidate = $individual_variant::new(sample.clone(), candidate_score.clone()); - new_population = vec![best_candidate; 1]; + let new_population = vec![best_candidate; 1]; self.late_scores.push_front(candidate_score); if self.late_scores.len() > self.late_acceptance_size { self.late_scores.pop_back(); } + (new_population, Some(best_deltas)) } else { - new_population = current_population.clone(); + (current_population.clone(), None) } - - return new_population; } #[getter] diff --git a/greyjack/src/agents/base/metaheuristic_bases/concrete_simulated_annealing_macros.rs b/greyjack/src/agents/base/metaheuristic_bases/concrete_simulated_annealing_macros.rs index 939dd01..9a7a884 100644 --- a/greyjack/src/agents/base/metaheuristic_bases/concrete_simulated_annealing_macros.rs +++ b/greyjack/src/agents/base/metaheuristic_bases/concrete_simulated_annealing_macros.rs @@ -166,7 +166,7 @@ macro_rules! build_concrete_simulated_annealing_base { sample: Vec, deltas: Vec>, scores: Vec<$score_type>, - ) -> Vec<$individual_variant> { + ) -> (Vec<$individual_variant>, Option>) { self.current_temperature = self.current_temperature.iter().map(|ct| { let mut new_temperature = *ct * self.cooling_rate; @@ -189,19 +189,17 @@ macro_rules! build_concrete_simulated_annealing_base { let random_value = self.random_sampler.sample(&mut self.random_generator); let mut sample = sample; - let new_population: Vec<$individual_variant>; - if (random_value < accept_proba) { - let candidate_deltas = &deltas[0]; - for (var_id, new_value) in candidate_deltas { + if (scores[0] <= current_population[0].score) || (random_value < accept_proba) { + let candidate_deltas = deltas[0].clone(); + for (var_id, new_value) in &candidate_deltas { sample[*var_id] = *new_value; } let candidate = $individual_variant::new(sample.clone(), scores[0].clone()); - new_population = vec![candidate; 1]; + let new_population = vec![candidate; 1]; + (new_population, Some(candidate_deltas)) } else { - new_population = current_population.clone(); + (current_population.clone(), None) } - - return new_population; } #[getter] diff --git a/greyjack/uv.lock b/greyjack/uv.lock index f50515b..40499d9 100644 --- a/greyjack/uv.lock +++ b/greyjack/uv.lock @@ -384,7 +384,7 @@ wheels = [ [[package]] name = "greyjack" -version = "0.3.2" +version = "0.3.3" source = { editable = "." } dependencies = [ { name = "bitarray" }, From 7c6b163f753ab16500e087b6e86991f6e96e951a Mon Sep 17 00:00:00 2001 From: CameleoGrey Date: Sun, 20 Jul 2025 05:34:32 +0300 Subject: [PATCH 3/4] v0.3.5 More examples with greynet. Currently it is suitable for problems without order planning. TODO: implement Chained Variables for problems like TSP, VRP, etc --- .../cloud_balancing/cotwin/CotComputer.py | 2 + .../cloud_balancing/cotwin/CotProcess.py | 3 +- .../persistence/CotwinBuilder.py | 16 +- .../score/GreynetScoreCalculatorCB.py | 93 ++++++++++ .../scripts/solve_cloud_balancing.py | 2 +- .../object_oriented/tsp_greynet/__init__.py | 0 .../tsp_greynet/cotwin/CotLocation.py | 11 ++ .../tsp_greynet/cotwin/CotStop.py | 14 ++ .../tsp_greynet/cotwin/TSPCotwin.py | 10 + .../tsp_greynet/cotwin/__init__.py | 0 .../tsp_greynet/domain/Location.py | 26 +++ .../tsp_greynet/domain/TravelSchedule.py | 102 +++++++++++ .../tsp_greynet/domain/Vehicle.py | 10 + .../tsp_greynet/domain/__init__.py | 0 .../tsp_greynet/persistence/CotwinBuilder.py | 70 +++++++ .../tsp_greynet/persistence/DomainBuilder.py | 173 ++++++++++++++++++ .../tsp_greynet/persistence/__init__.py | 4 + .../score/GreynetScoreCalculatorTSP.py | 75 ++++++++ .../tsp_greynet/score/__init__.py | 0 .../tsp_greynet/scripts/__init__.py | 0 .../tsp_greynet/scripts/solve_tsp.py | 66 +++++++ greyjack/Cargo.lock | 2 +- greyjack/Cargo.toml | 2 +- .../score_requesters/OOPScoreRequester.py | 4 +- greyjack/pyproject.toml | 2 +- 25 files changed, 677 insertions(+), 10 deletions(-) create mode 100644 examples/object_oriented/cloud_balancing/score/GreynetScoreCalculatorCB.py create mode 100644 examples/object_oriented/tsp_greynet/__init__.py create mode 100644 examples/object_oriented/tsp_greynet/cotwin/CotLocation.py create mode 100644 examples/object_oriented/tsp_greynet/cotwin/CotStop.py create mode 100644 examples/object_oriented/tsp_greynet/cotwin/TSPCotwin.py create mode 100644 examples/object_oriented/tsp_greynet/cotwin/__init__.py create mode 100644 examples/object_oriented/tsp_greynet/domain/Location.py create mode 100644 examples/object_oriented/tsp_greynet/domain/TravelSchedule.py create mode 100644 examples/object_oriented/tsp_greynet/domain/Vehicle.py create mode 100644 examples/object_oriented/tsp_greynet/domain/__init__.py create mode 100644 examples/object_oriented/tsp_greynet/persistence/CotwinBuilder.py create mode 100644 examples/object_oriented/tsp_greynet/persistence/DomainBuilder.py create mode 100644 examples/object_oriented/tsp_greynet/persistence/__init__.py create mode 100644 examples/object_oriented/tsp_greynet/score/GreynetScoreCalculatorTSP.py create mode 100644 examples/object_oriented/tsp_greynet/score/__init__.py create mode 100644 examples/object_oriented/tsp_greynet/scripts/__init__.py create mode 100644 examples/object_oriented/tsp_greynet/scripts/solve_tsp.py diff --git a/examples/object_oriented/cloud_balancing/cotwin/CotComputer.py b/examples/object_oriented/cloud_balancing/cotwin/CotComputer.py index 56ff79c..f5cf36d 100644 --- a/examples/object_oriented/cloud_balancing/cotwin/CotComputer.py +++ b/examples/object_oriented/cloud_balancing/cotwin/CotComputer.py @@ -1,6 +1,8 @@ +from greyjack.score_calculation.greynet.greynet_fact import greynet_fact +@greynet_fact class CotComputer(): def __init__(self, computer_id, cpu_power, memory_size, network_bandwidth, cost): diff --git a/examples/object_oriented/cloud_balancing/cotwin/CotProcess.py b/examples/object_oriented/cloud_balancing/cotwin/CotProcess.py index f1d369c..9e4c184 100644 --- a/examples/object_oriented/cloud_balancing/cotwin/CotProcess.py +++ b/examples/object_oriented/cloud_balancing/cotwin/CotProcess.py @@ -1,6 +1,7 @@ +from greyjack.score_calculation.greynet.greynet_fact import greynet_fact - +@greynet_fact class CotProcess(): def __init__(self, process_id, cpu_power_req, memory_size_req, network_bandwidth_req, computer_id): diff --git a/examples/object_oriented/cloud_balancing/persistence/CotwinBuilder.py b/examples/object_oriented/cloud_balancing/persistence/CotwinBuilder.py index 29136d5..eb5639b 100644 --- a/examples/object_oriented/cloud_balancing/persistence/CotwinBuilder.py +++ b/examples/object_oriented/cloud_balancing/persistence/CotwinBuilder.py @@ -6,14 +6,15 @@ from examples.object_oriented.cloud_balancing.cotwin.CotComputer import CotComputer from examples.object_oriented.cloud_balancing.score.PlainScoreCalculatorCB import PlainScoreCalculatorCB from examples.object_oriented.cloud_balancing.score.IncrementalScoreCalculatorCB import IncrementalScoreCalculatorCB +from examples.object_oriented.cloud_balancing.score.GreynetScoreCalculatorCB import GreynetScoreCalculatorCB import numpy as np from numba import jit class CotwinBuilder(CotwinBuilderBase): - def __init__(self, use_incremental_score_calculator, use_greed_init): + def __init__(self, scorer_name, use_greed_init): - self.use_incremental_score_calculator = use_incremental_score_calculator + self.scorer_name = scorer_name self.use_greed_init = use_greed_init pass @@ -25,15 +26,22 @@ def build_cotwin(self, domain_model, is_already_initialized): cotwin_model.add_planning_entities_list(self._build_planning_processes(domain_model), "processes") cotwin_model.add_problem_facts_list(self._build_problem_fact_computers(domain_model), "computers") - if self.use_incremental_score_calculator: + if self.scorer_name == "plain": + score_calculator = PlainScoreCalculatorCB() + if self.scorer_name == "pseudo": score_calculator = IncrementalScoreCalculatorCB() #to avoid joining, fast get common info only score_calculator.utility_objects["processes_info"] = self._build_processes_common_info(domain_model) score_calculator.utility_objects["computers_info"] = self._build_computers_common_info(domain_model) score_calculator.utility_objects["computers_costs"] = self._build_computers_costs(domain_model) + elif self.scorer_name == "greynet_plain": + score_calculator = GreynetScoreCalculatorCB() + score_calculator.is_incremental = False + elif self.scorer_name == "greynet_incremental": + score_calculator = GreynetScoreCalculatorCB() else: - score_calculator = PlainScoreCalculatorCB() + raise ValueError("Available score calculators: plain, pseudo, greynet") cotwin_model.set_score_calculator( score_calculator ) diff --git a/examples/object_oriented/cloud_balancing/score/GreynetScoreCalculatorCB.py b/examples/object_oriented/cloud_balancing/score/GreynetScoreCalculatorCB.py new file mode 100644 index 0000000..36b2616 --- /dev/null +++ b/examples/object_oriented/cloud_balancing/score/GreynetScoreCalculatorCB.py @@ -0,0 +1,93 @@ + +from greyjack.score_calculation.greynet.builder import ConstraintBuilder, Collectors, JoinerType +from greyjack.score_calculation.scores.HardSoftScore import HardSoftScore +from greyjack.score_calculation.scores.ScoreVariants import ScoreVariants +from greyjack.score_calculation.score_calculators.GreynetScoreCalculator import GreynetScoreCalculator + +from examples.object_oriented.cloud_balancing.cotwin.CotProcess import CotProcess +from examples.object_oriented.cloud_balancing.cotwin.CotComputer import CotComputer +import traceback + + +class GreynetScoreCalculatorCB(GreynetScoreCalculator): + + def __init__(self): + + cb = ConstraintBuilder(name="cloud_balancing_greynet_constraint_builder", score_class=HardSoftScore) + self.add_constraints(cb) + + super().__init__(constraint_builder=cb, score_variant=ScoreVariants.HardSoftScore) + + + def add_constraints(self, cb: ConstraintBuilder): + + @cb.constraint("required_cpu", default_weight=1.0) + def required_cpu_constraint(): + return ( + cb.for_each(CotProcess) + .group_by( + group_key_function=lambda process: process.computer_id, + collector_supplier=Collectors.sum(lambda process: process.cpu_power_req) + ) + .join( + cb.for_each(CotComputer), + JoinerType.EQUAL, + lambda computer_id, total_cpu: computer_id, + lambda computer: computer.computer_id + ) + .filter(lambda cid, total_cpu, comp: total_cpu > comp.cpu_power) + .penalize_hard(lambda cid, total_cpu, comp: total_cpu - comp.cpu_power) + ) + + @cb.constraint("required_memory", default_weight=1.0) + def required_memory_constraint(): + return ( + cb.for_each(CotProcess) + .group_by( + group_key_function=lambda process: process.computer_id, + collector_supplier=Collectors.sum(lambda process: process.memory_size_req) + ) + .join( + cb.for_each(CotComputer), + JoinerType.EQUAL, + lambda computer_id, total_mem: computer_id, + lambda computer: computer.computer_id + ) + .filter(lambda cid, total_mem, comp: total_mem > comp.memory_size) + .penalize_hard(lambda cid, total_mem, comp: total_mem - comp.memory_size) + ) + + @cb.constraint("required_network", default_weight=1.0) + def required_network_constraint(): + return ( + cb.for_each(CotProcess) + .group_by( + group_key_function=lambda process: process.computer_id, + collector_supplier=Collectors.sum(lambda process: process.network_bandwidth_req) + ) + .join( + cb.for_each(CotComputer), + JoinerType.EQUAL, + lambda computer_id, total_net: computer_id, + lambda computer: computer.computer_id + ) + .filter(lambda cid, total_net, comp: total_net > comp.network_bandwidth) + .penalize_hard(lambda cid, total_net, comp: total_net - comp.network_bandwidth) + ) + + @cb.constraint("computer_cost", default_weight=1.0) + def computer_cost_constraint(): + return ( + cb.for_each(CotProcess) + .group_by( + group_key_function=lambda process: process.computer_id, + collector_supplier=Collectors.to_list() # We just need to know the computer is used + ) + .join( + cb.for_each(CotComputer), + JoinerType.EQUAL, + lambda computer_id, processes: computer_id, + lambda computer: computer.computer_id + ) + .penalize_soft(lambda cid, procs, comp: comp.cost) + ) \ No newline at end of file diff --git a/examples/object_oriented/cloud_balancing/scripts/solve_cloud_balancing.py b/examples/object_oriented/cloud_balancing/scripts/solve_cloud_balancing.py index fa78a72..f023e6a 100644 --- a/examples/object_oriented/cloud_balancing/scripts/solve_cloud_balancing.py +++ b/examples/object_oriented/cloud_balancing/scripts/solve_cloud_balancing.py @@ -30,7 +30,7 @@ domain_builder = DomainBuilder(file_path) - cotwin_builder = CotwinBuilder(use_incremental_score_calculator=True, use_greed_init=True) + cotwin_builder = CotwinBuilder(scorer_name="pseudo", use_greed_init=True) #termination_strategy = StepsLimit(step_count_limit=1000) #termination_strategy = TimeSpentLimit(time_seconds_limit=60) diff --git a/examples/object_oriented/tsp_greynet/__init__.py b/examples/object_oriented/tsp_greynet/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/object_oriented/tsp_greynet/cotwin/CotLocation.py b/examples/object_oriented/tsp_greynet/cotwin/CotLocation.py new file mode 100644 index 0000000..10af748 --- /dev/null +++ b/examples/object_oriented/tsp_greynet/cotwin/CotLocation.py @@ -0,0 +1,11 @@ +from dataclasses import dataclass, field +from greyjack.score_calculation.greynet.greynet_fact import greynet_fact +from greyjack.variables.GJInteger import GJInteger + +@greynet_fact +@dataclass +class CotLocation: + """Represents a physical location with coordinates. This is a problem fact.""" + location_id: int + latitude: float + longitude: float \ No newline at end of file diff --git a/examples/object_oriented/tsp_greynet/cotwin/CotStop.py b/examples/object_oriented/tsp_greynet/cotwin/CotStop.py new file mode 100644 index 0000000..36ff935 --- /dev/null +++ b/examples/object_oriented/tsp_greynet/cotwin/CotStop.py @@ -0,0 +1,14 @@ + +from dataclasses import dataclass, field +from greyjack.score_calculation.greynet.greynet_fact import greynet_fact +from greyjack.variables.GJInteger import GJInteger + +@greynet_fact +@dataclass +class CotStop: + """ + Represents a stop that must be visited. This is our main Planning Entity. + The planning variable 'previous_stop_id' determines which stop comes before this one in the tour. + """ + location_id: int + previous_stop_id: GJInteger = field(default=None) \ No newline at end of file diff --git a/examples/object_oriented/tsp_greynet/cotwin/TSPCotwin.py b/examples/object_oriented/tsp_greynet/cotwin/TSPCotwin.py new file mode 100644 index 0000000..d8206a5 --- /dev/null +++ b/examples/object_oriented/tsp_greynet/cotwin/TSPCotwin.py @@ -0,0 +1,10 @@ + +from greyjack.cotwin.CotwinBase import CotwinBase + +class TSPCotwin( CotwinBase ): + + def __init__(self): + super().__init__() + + pass + diff --git a/examples/object_oriented/tsp_greynet/cotwin/__init__.py b/examples/object_oriented/tsp_greynet/cotwin/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/object_oriented/tsp_greynet/domain/Location.py b/examples/object_oriented/tsp_greynet/domain/Location.py new file mode 100644 index 0000000..8efd5f2 --- /dev/null +++ b/examples/object_oriented/tsp_greynet/domain/Location.py @@ -0,0 +1,26 @@ + +import math + +class Location(): + def __init__(self, id, name, latitude, longitude, distances_to_other_locations_dict): + + self.id = id + self.name = name + self.latitude = latitude + self.longitude = longitude + self.distances_to_other_locations_dict = None + + pass + + def __str__(self): + return "Location id: " + str(self.id) + " | " + self.name + ": " + "lat=" + str(self.latitude) + ", " + "lon=" + str(self.longitude) + + def get_distance_to_other_location(self, other_location): + + if self.distances_to_other_locations_dict is None: + distance = math.sqrt((other_location.latitude - self.latitude)**2 + (other_location.longitude - self.longitude)**2) + else: + distance = self.distances_to_other_locations_dict[ other_location.name ] + + return distance + diff --git a/examples/object_oriented/tsp_greynet/domain/TravelSchedule.py b/examples/object_oriented/tsp_greynet/domain/TravelSchedule.py new file mode 100644 index 0000000..d1572b6 --- /dev/null +++ b/examples/object_oriented/tsp_greynet/domain/TravelSchedule.py @@ -0,0 +1,102 @@ +import numpy as np +import matplotlib.pyplot as plt + +class TravelSchedule(): + def __init__(self, name, vehicle, locations_list, distance_matrix): + + self.name = name + self.vehicle = vehicle + self.locations_list = locations_list + self.distance_matrix = distance_matrix + + pass + + def get_unique_stops_count(self): + unique_stops = set(self.vehicle.trip_path) + unique_stops_count = len(unique_stops) + return unique_stops_count + + def get_travel_distance(self): + + depot = self.vehicle.depot + trip_path = self.vehicle.trip_path + if trip_path is None or trip_path == []: + raise Exception("Vehicle trip_path is not initialized. Probably, a TSP task isn't solved yet or domain model isn't updated.") + + depot_to_first_stop_distance = depot.get_distance_to_other_location( trip_path[0] ) + last_stop_to_depot_distance = trip_path[-1].get_distance_to_other_location( depot ) + + interim_stops_distance = 0 + for i in range(1, len(trip_path)): + stop_from = trip_path[i-1] + stop_to = trip_path[i] + current_distance = stop_from.get_distance_to_other_location( stop_to ) + interim_stops_distance += current_distance + + travel_distance = depot_to_first_stop_distance + interim_stops_distance + last_stop_to_depot_distance + + return travel_distance + + def print_metrics(self): + print("Solution distance: {}".format(self.get_travel_distance())) + print("Unique stops (excluding depot): {}".format(self.get_unique_stops_count())) + + def print_path(self): + path_names_string = self.build_string_of_path_names() + path_ids_string = self.build_string_of_path_ids() + + print( path_names_string ) + print( path_ids_string ) + + def build_string_of_path_names(self): + path_names_string = [str(self.vehicle.depot.name)] + for stop in self.vehicle.trip_path: + path_names_string.append( str(stop.name) ) + path_names_string.append( str(self.vehicle.depot.name) ) + path_names_string = " --> ".join( path_names_string ) + return path_names_string + + def build_string_of_path_ids(self): + path_ids_string = [str(self.vehicle.depot.id)] + for stop in self.vehicle.trip_path: + path_ids_string.append( str(stop.id) ) + path_ids_string.append( str(self.vehicle.depot.id) ) + path_ids_string = " --> ".join( path_ids_string ) + return path_ids_string + + + def plot_path(self, image_file_path=None, dpi=200): + + x_coordinates = [] + y_coordinates = [] + labels = [] + for location in self.locations_list: + x = location.latitude + y = location.longitude + label = location.name + + x_coordinates.append( x ) + y_coordinates.append( y ) + labels.append( label ) + + plt.scatter( x=x_coordinates, y=y_coordinates, s=1) + for x, y, label in zip(x_coordinates, y_coordinates, labels): + plt.text( x, y, label, fontsize=8, fontfamily="calibri" ) + + edges_x = [self.vehicle.depot.latitude] + edges_y = [self.vehicle.depot.longitude] + trip_path = self.vehicle.trip_path + for stop_point in trip_path: + edges_x.append( stop_point.latitude ) + edges_y.append( stop_point.longitude ) + + edges_x.append(self.vehicle.depot.latitude) + edges_y.append(self.vehicle.depot.longitude) + + plt.plot( edges_x, edges_y, linewidth=0.5 ) + + if image_file_path is None: + plt.show() + else: + plt.savefig(image_file_path, dpi=dpi) + plt.close() \ No newline at end of file diff --git a/examples/object_oriented/tsp_greynet/domain/Vehicle.py b/examples/object_oriented/tsp_greynet/domain/Vehicle.py new file mode 100644 index 0000000..fb9c08b --- /dev/null +++ b/examples/object_oriented/tsp_greynet/domain/Vehicle.py @@ -0,0 +1,10 @@ + + +class Vehicle(): + + def __init__(self, depot, trip_path): + + self.depot = depot + self.trip_path = trip_path + + pass \ No newline at end of file diff --git a/examples/object_oriented/tsp_greynet/domain/__init__.py b/examples/object_oriented/tsp_greynet/domain/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/object_oriented/tsp_greynet/persistence/CotwinBuilder.py b/examples/object_oriented/tsp_greynet/persistence/CotwinBuilder.py new file mode 100644 index 0000000..59fa49c --- /dev/null +++ b/examples/object_oriented/tsp_greynet/persistence/CotwinBuilder.py @@ -0,0 +1,70 @@ +import random +import numpy as np + +from examples.object_oriented.tsp_greynet.cotwin.CotStop import CotStop +from examples.object_oriented.tsp_greynet.cotwin.CotLocation import CotLocation +from examples.object_oriented.tsp_greynet.cotwin.TSPCotwin import TSPCotwin +from examples.object_oriented.tsp_greynet.score.GreynetScoreCalculatorTSP import GreynetScoreCalculatorTSP + +from greyjack.persistence.CotwinBuilderBase import CotwinBuilderBase +from greyjack.variables.GJInteger import GJInteger + + +class CotwinBuilder(CotwinBuilderBase): + + def __init__(self): + + super().__init__() + + pass + + def build_cotwin(self, domain_model, is_already_initialized): + + domain_locations = domain_model.locations_list + distance_matrix = domain_model.distance_matrix + + problem_fact_locations = self._build_problem_fact_locations(domain_locations) + planning_stops = self._build_planning_stops(problem_fact_locations) + score_calculator = GreynetScoreCalculatorTSP(problem_fact_locations, distance_matrix) + + cotwin = TSPCotwin() + cotwin.add_planning_entities_list(planning_stops, "planning_stops") + cotwin.set_score_calculator(score_calculator) + + return cotwin + + def _build_problem_fact_locations(self, domain_locations): + + problem_fact_locations = [] + for i, domain_location in enumerate(domain_locations): + problem_fact_location = CotLocation( + location_id=i, + latitude=domain_location.latitude, + longitude=domain_location.longitude + ) + problem_fact_locations.append(problem_fact_location) + + return problem_fact_locations + + def _build_planning_stops(self, locations): + """Creates a Stop entity for each location that needs to be visited.""" + planning_stops = [] + + n_locations = len(locations) + + for i, loc in enumerate(locations): + previous_stop_var = GJInteger( + lower_bound=0, + upper_bound=n_locations - 1, + frozen=False, + initial_value=n_locations - 1 - i + ) + + # Create the Stop entity + planning_stop = CotStop( + location_id=loc.location_id, + previous_stop_id=previous_stop_var + ) + planning_stops.append(planning_stop) + + return planning_stops \ No newline at end of file diff --git a/examples/object_oriented/tsp_greynet/persistence/DomainBuilder.py b/examples/object_oriented/tsp_greynet/persistence/DomainBuilder.py new file mode 100644 index 0000000..9023d7d --- /dev/null +++ b/examples/object_oriented/tsp_greynet/persistence/DomainBuilder.py @@ -0,0 +1,173 @@ + + +import re +import numpy as np +from numba import jit +from examples.object_oriented.tsp_greynet.domain.TravelSchedule import TravelSchedule +from examples.object_oriented.tsp_greynet.domain.Vehicle import Vehicle +from examples.object_oriented.tsp_greynet.domain.Location import Location +from greyjack.persistence.DomainBuilderBase import DomainBuilderBase + +class DomainBuilder(DomainBuilderBase): + + def __init__(self, file_path): + + super().__init__() + + self.file_path = file_path + + pass + + def build_domain_from_scratch(self): + + read_file = self._read_tsp_file( self.file_path ) + + dataset_name = read_file["metadata"]["dataset_name"] + locations_list = read_file["locations_list"] + distance_matrix = None + if "distance_matrix" in read_file: + distance_matrix = read_file["distance_matrix"] + for i in range(len(locations_list)): + distances_to_other_locations_dict = {} + for j in range(len(locations_list)): + current_distance = distance_matrix[i][j] + to_location_name = locations_list[j].name + distances_to_other_locations_dict[to_location_name] = current_distance + locations_list[i].distances_to_other_locations_dict = distances_to_other_locations_dict + else: + distance_matrix = self._build_distance_matrix( locations_list ) + + depot = locations_list[0] + vehicle = Vehicle(depot, trip_path=None) + + domain_model = TravelSchedule(name=dataset_name, + vehicle=vehicle, + locations_list=locations_list, + distance_matrix=distance_matrix) + + return domain_model + + def build_from_solution(self, solution, initial_domain=None): + + # TODO: write correct logic to extract path (no need before starting work on chained variables) + raise Exception("TODO: write correct logic to extract path (no need before starting work on chained variables)") + + domain = self.build_domain_from_scratch() + path_stop_ids = [] + for planning_stop_name in solution.variable_values_dict: + location_id = solution.variable_values_dict[planning_stop_name] + path_stop_ids.append( location_id ) + + trip_path = [] + for location_id in path_stop_ids: + current_location = domain.locations_list[location_id] + trip_path.append( current_location ) + domain.vehicle.trip_path = trip_path + + return domain + + def build_from_domain(self, domain): + return super().build_from_domain(domain) + + def _build_distance_matrix(self, locations_list): + + @staticmethod + @jit(nopython=True, cache=True) + def compute_distance_matrix(latitudes, longitudes, n_locations): + distance_matrix = np.zeros( (n_locations, n_locations), dtype=np.int64 ) + for i in range(n_locations): + for j in range(n_locations): + distance_from_to = np.sqrt((latitudes[i] - latitudes[j])**2 + (longitudes[i] - longitudes[j])**2) + #distance_matrix[i][j] = round(1000 * distance_from_to, 0) + distance_matrix[i][j] = round(distance_from_to, 4) + return distance_matrix + + n_locations = len(locations_list) + latitudes = np.zeros((n_locations, ), dtype=np.float64) + longitudes = np.zeros((n_locations, ), dtype=np.float64) + for i, location in enumerate(locations_list): + latitudes[i] = location.latitude + longitudes[i] = location.longitude + + distance_matrix = compute_distance_matrix(latitudes, longitudes, n_locations) + + return distance_matrix + + def _read_tsp_file(self, file_path): + + def read_metadata( file_pointer ): + metadata = {} + readed_line = "" + while True: + readed_line = tsp_file.readline() + if "NODE_COORD_SECTION" in readed_line: + break + + if "NAME" in readed_line: + dataset_name = readed_line.split(" ")[-1] + dataset_name = dataset_name.replace("\n", "") + metadata["dataset_name"] = dataset_name + + if "EDGE_WEIGHT_TYPE" in readed_line: + distance_type = readed_line.split(" ")[-1] + distance_type = distance_type.replace("\n", "") + metadata["distance_type"] = distance_type + + pass + + return metadata + + def read_locations_list( file_pointer ): + locations_list = [] + readed_line = "" + while True: + readed_line = file_pointer.readline() + if "EOF" in readed_line or "EDGE_WEIGHT_SECTION" in readed_line: + break + + readed_line = readed_line.strip() + readed_line = re.sub(" +", " ", readed_line) + readed_line = readed_line.split(" ") + id = int( readed_line[0] ) + latitude = float( readed_line[1] ) + longitude = float( readed_line[2] ) + if len(readed_line) > 3: + name = readed_line[3].replace("\n", "") + else: + name = str(id) + current_location = Location( id=id, latitude=latitude, longitude=longitude, name=name, distances_to_other_locations_dict=None ) + locations_list.append( current_location ) + + return locations_list + + def read_distance_matrix( file_pointer ): + distance_matrix = [] + readed_line = "" + while True: + readed_line = file_pointer.readline() + if "EOF" in readed_line: + break + + readed_line = readed_line.split(" ") + readed_line.pop(-1) + matrix_row = [float(value) for value in readed_line] + matrix_row = np.array( matrix_row, dtype=np.float32 ) + distance_matrix.append( matrix_row ) + + distance_matrix = np.array( distance_matrix, dtype=np.float32 ) + + return distance_matrix + + read_file_dict = {} + with open( file_path, "r" ) as tsp_file: + metadata = read_metadata( tsp_file ) + locations_list = read_locations_list( tsp_file ) + + if metadata["distance_type"] != "EUC_2D": + distance_matrix = read_distance_matrix( tsp_file ) + read_file_dict["distance_matrix"] = distance_matrix + + read_file_dict["metadata"] = metadata + read_file_dict["locations_list"] = locations_list + + return read_file_dict diff --git a/examples/object_oriented/tsp_greynet/persistence/__init__.py b/examples/object_oriented/tsp_greynet/persistence/__init__.py new file mode 100644 index 0000000..af50db8 --- /dev/null +++ b/examples/object_oriented/tsp_greynet/persistence/__init__.py @@ -0,0 +1,4 @@ + + +from examples.object_oriented.tsp_greynet.persistence.CotwinBuilder import CotwinBuilder +from examples.object_oriented.tsp_greynet.persistence.DomainBuilder import DomainBuilder \ No newline at end of file diff --git a/examples/object_oriented/tsp_greynet/score/GreynetScoreCalculatorTSP.py b/examples/object_oriented/tsp_greynet/score/GreynetScoreCalculatorTSP.py new file mode 100644 index 0000000..4c6d3ea --- /dev/null +++ b/examples/object_oriented/tsp_greynet/score/GreynetScoreCalculatorTSP.py @@ -0,0 +1,75 @@ + + +from typing import List, Dict + +from examples.object_oriented.tsp_greynet.cotwin.CotStop import CotStop +from examples.object_oriented.tsp_greynet.cotwin.CotLocation import CotLocation + +from greyjack.score_calculation.greynet.builder import ConstraintBuilder, Collectors +from greyjack.score_calculation.scores.HardSoftScore import HardSoftScore +from greyjack.score_calculation.scores.ScoreVariants import ScoreVariants +from greyjack.score_calculation.score_calculators.GreynetScoreCalculator import GreynetScoreCalculator + + +class GreynetScoreCalculatorTSP(GreynetScoreCalculator): + """ + Uses the Greynet rule engine to calculate the score for the TSP problem. + """ + def __init__(self, locations: List[CotLocation], distance_matrix: Dict[int, Dict[int, float]]): + """ + Initializes the calculator by defining all constraints. + + Args: + locations (List[CotLocation]): The list of all locations (including depot at id 0). + distance_matrix (Dict): A pre-computed matrix for O(1) distance lookups. + """ + self.locations = locations + self.distance_matrix = distance_matrix + + self.stop_location_ids = {int(loc.location_id) for loc in locations} + + # Initialize the constraint builder with our score class + builder = ConstraintBuilder(score_class=HardSoftScore) + self.define_constraints(builder) + + # Pass the fully configured builder to the parent constructor + super().__init__(constraint_builder=builder, score_variant=ScoreVariants.HardSoftScore) + + def define_constraints(self, cb: ConstraintBuilder): + """ + Defines the hard and soft constraints for the TSP problem. + + Args: + cb (ConstraintBuilder): The greynet constraint builder instance. + """ + + # --- HARD CONSTRAINTS --- + + # Constraint 1: A stop cannot be its own predecessor. + @cb.constraint("self_predecessor", 1.0) + def self_predecessor(): + return (cb.for_each(CotStop) + .filter(lambda stop: int(stop.location_id) == int(stop.previous_stop_id)) + .penalize_hard(1) + ) + + # Constraint 2: Every stop must be a predecessor to exactly one other stop. + @cb.constraint("predecessor_count", 1.0) + def predecessor_count(): + return (cb.for_each(CotStop) + .group_by(lambda stop: int(stop.previous_stop_id), Collectors.count()) + .filter(lambda prev_id, count: count != 1) + .penalize_hard(lambda prev_id, count: abs(count - 1)) + ) + + # TODO: Implement chained variables to eliminate subtours... + + # --- SOFT CONSTRAINT (OBJECTIVE) --- + + # Constraint 3: Minimize the total distance of the tour. + @cb.constraint("total_distance", 1.0) + def total_distance(): + return (cb.for_each(CotStop) + # The penalty is the distance between this stop and its predecessor. + .penalize_soft(lambda stop: self.distance_matrix[int(stop.previous_stop_id)][int(stop.location_id)]) + ) \ No newline at end of file diff --git a/examples/object_oriented/tsp_greynet/score/__init__.py b/examples/object_oriented/tsp_greynet/score/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/object_oriented/tsp_greynet/scripts/__init__.py b/examples/object_oriented/tsp_greynet/scripts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/object_oriented/tsp_greynet/scripts/solve_tsp.py b/examples/object_oriented/tsp_greynet/scripts/solve_tsp.py new file mode 100644 index 0000000..e068ef3 --- /dev/null +++ b/examples/object_oriented/tsp_greynet/scripts/solve_tsp.py @@ -0,0 +1,66 @@ +from copy import deepcopy +from pathlib import Path +import os +import sys + +# To launch normally from console +script_dir_path = Path(os.path.dirname(os.path.realpath(__file__))) +project_dir_id = script_dir_path.parts.index("greyjack-solver-python") +project_dir_path = Path(*script_dir_path.parts[:project_dir_id+1]) +sys.path.append(str(project_dir_path)) + +from examples.object_oriented.tsp_greynet.persistence import DomainBuilder +from examples.object_oriented.tsp_greynet.persistence import CotwinBuilder +from greyjack.agents.termination_strategies import * +from greyjack.agents.base.LoggingLevel import LoggingLevel +from greyjack.agents.base.ParallelizationBackend import ParallelizationBackend +from greyjack import SolverOOP +from greyjack.agents import * + +if __name__ == "__main__": + + # OptaPlanner/TSPLIB datasets and corresponding achieved results + data_dir_path = Path(project_dir_path, "data", "tsp", "data", "import") + file_path = Path(data_dir_path, "cook", "air", "dj38.tsp") # 6659 - optimum + #file_path = Path(data_dir_path, "belgium", "air", "belgium-n50.tsp") # ~12.2 - optimum + #file_path = Path(data_dir_path, "cook", "air", "st70.tsp") # 682.57 - optimum + #file_path = Path(data_dir_path, "belgium", "road-km", "belgium-road-km-n100.tsp") # 1727.262 - optimum, 2089 - first_fit + #file_path = Path(data_dir_path, "tsplib", "a280.tsp") # 2579 - optimal + #file_path = Path(data_dir_path, "cook", "air", "pcb442.tsp") #optimum: 50778; first_fit: ~63k + #file_path = Path(data_dir_path, "cook", "air", "lu980.tsp") + #file_path = Path(data_dir_path, "other", "air", "usa_tx_2743.tsp") #optimum: ~282; first_fit: ~338 + #file_path = Path(data_dir_path, "belgium", "air", "belgium-n2750.tsp") + #file_path = Path(data_dir_path, "tsplib", "fnl4461.tsp") #optimum: 182566; first_fit: ~230k + #file_path = Path(data_dir_path, "cook", "air", "gr9882.tsp") #optimum: 300899; first_fit: ~400k + + domain_builder = DomainBuilder(file_path) + cotwin_builder = CotwinBuilder() + + #termination_strategy = StepsLimit(step_count_limit=1000) + termination_strategy = TimeSpentLimit(time_seconds_limit=2) + #termination_strategy = ScoreNoImprovement(time_seconds_limit=15) + #termination_strategy = ScoreLimit(score_to_compare=[0]) + agent = TabuSearch(neighbours_count=128, tabu_entity_rate=0.5, + mutation_rate_multiplier=None, move_probas=[0.0, 0.2, 0.2, 0.2, 0.2, 0.2], + migration_frequency=10, termination_strategy=termination_strategy) + """agent = GeneticAlgorithm(population_size=128, crossover_probability=0.5, p_best_rate=0.05, + tabu_entity_rate=0.2, mutation_rate_multiplier=1.0, move_probas=[0.0, 0.2, 0.2, 0.2, 0.2, 0.2], + migration_rate=0.00001, migration_frequency=10, termination_strategy=termination_strategy)""" + """agent = LateAcceptance(late_acceptance_size=64, tabu_entity_rate=0.2, + mutation_rate_multiplier=None, move_probas=[0.0, 0.2, 0.2, 0.2, 0.2, 0.2], + termination_strategy=termination_strategy)""" + """agent = SimulatedAnnealing(initial_temperature=[1.0, 1.0], cooling_rate=0.9999, tabu_entity_rate=0.2, + mutation_rate_multiplier=None, move_probas=[0, 0.2, 0.2, 0.2, 0.2, 0.2], + migration_frequency=10, termination_strategy=termination_strategy)""" + + solver = SolverOOP(domain_builder, cotwin_builder, agent, + ParallelizationBackend.Threading, LoggingLevel.FreshOnly, + n_jobs=1, score_precision=[0, 0]) + solution = solver.solve() + + domain = domain_builder.build_from_solution(solution) + domain.print_metrics() + domain.print_path() + #domain.plot_path() + + print("done") diff --git a/greyjack/Cargo.lock b/greyjack/Cargo.lock index 4d1bbb1..d4784b1 100644 --- a/greyjack/Cargo.lock +++ b/greyjack/Cargo.lock @@ -579,7 +579,7 @@ checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" [[package]] name = "greyjack" -version = "0.3.4" +version = "0.3.5" dependencies = [ "chrono", "ndarray", diff --git a/greyjack/Cargo.toml b/greyjack/Cargo.toml index 1861ea4..a67e41d 100644 --- a/greyjack/Cargo.toml +++ b/greyjack/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "greyjack" -version = "0.3.4" +version = "0.3.5" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/greyjack/greyjack/score_calculation/score_requesters/OOPScoreRequester.py b/greyjack/greyjack/score_calculation/score_requesters/OOPScoreRequester.py index 1343eef..b0b5ae2 100644 --- a/greyjack/greyjack/score_calculation/score_requesters/OOPScoreRequester.py +++ b/greyjack/greyjack/score_calculation/score_requesters/OOPScoreRequester.py @@ -70,9 +70,11 @@ def build_initialized_entity(self, entity): if type(attribute_value) in {GJFloat, GJInteger, GJBinary}: value = attribute_value.planning_variable.initial_value - if value is None: raise ValueError("All planning variables must have initial value for scoring by greynet") + + #if type(attribute_value) in {GJInteger, GJBinary}: + # value = int(value) else: value = attribute_value diff --git a/greyjack/pyproject.toml b/greyjack/pyproject.toml index 3784797..04d3fe6 100644 --- a/greyjack/pyproject.toml +++ b/greyjack/pyproject.toml @@ -7,7 +7,7 @@ features = ["pyo3/extension-module"] [project] name = "greyjack" -version = "0.3.4" +version = "0.3.5" requires-python = ">=3.9" dependencies = [ "bitarray>=3.5.0", From bc3c1b879fa4c4c6f6d8c7b83f4c96bd2889f6cc Mon Sep 17 00:00:00 2001 From: CameleoGrey Date: Sun, 20 Jul 2025 06:43:49 +0300 Subject: [PATCH 4/4] v0.3.6 Refactor before merging into main --- README.md | 6 +- .../greynet/greynet_example_all_collectors.py | 55 -- examples/greynet/greynet_example_core.py | 28 - .../greynet/greynet_example_temporal_1.py | 225 ------ .../greynet/greynet_example_temporal_2.py | 177 ----- .../scripts/solve_cloud_balancing.py | 2 +- .../solve_nqueens_greynet_experimental.py | 2 +- greyjack/Cargo.lock | 2 +- greyjack/Cargo.toml | 2 +- greyjack/README.md | 12 +- greyjack/greyjack/agents/base/Agent.py | 70 -- .../score_calculation/greynet/builder.py | 28 - .../greynet/collectors/temporal_collectors.py | 180 ----- .../connected_range_tracker.py | 105 --- .../constraint_tools/consecutive_set_tree.py | 134 ---- .../greynet/nodes/sequence_pattern_node.py | 164 ---- .../greynet/nodes/sliding_window_node.py | 120 --- .../greynet/streams/stream.py | 65 +- .../greynet/streams/stream_definition.py | 59 +- .../GreynetScoreCalculator.py | 81 +- .../score_requesters/OOPScoreRequester.py | 21 +- greyjack/pyproject.toml | 3 +- greyjack/uv.lock | 698 +----------------- 23 files changed, 20 insertions(+), 2219 deletions(-) delete mode 100644 examples/greynet/greynet_example_temporal_1.py delete mode 100644 examples/greynet/greynet_example_temporal_2.py delete mode 100644 greyjack/greyjack/score_calculation/greynet/collectors/temporal_collectors.py delete mode 100644 greyjack/greyjack/score_calculation/greynet/constraint_tools/connected_range_tracker.py delete mode 100644 greyjack/greyjack/score_calculation/greynet/constraint_tools/consecutive_set_tree.py delete mode 100644 greyjack/greyjack/score_calculation/greynet/nodes/sequence_pattern_node.py delete mode 100644 greyjack/greyjack/score_calculation/greynet/nodes/sliding_window_node.py diff --git a/README.md b/README.md index 7c04f1f..dbd0cd2 100644 --- a/README.md +++ b/README.md @@ -46,11 +46,11 @@ maturin develop --release # RoadMap - Types, arguments validation - Write docs -- Tests, tests, tests... +- Tests, tests, tests... + integration wtih CI/CD - Composite termination criterion (for example: solving limit minutes N AND score not improving M seconds) - Multi-level score - Custom moves support -- Try to impove incremental (pseudo-incremental) score calculation mechanism (caching, no clonning, etc) - Website - Useful text materials, guides, presentations -- Score explainer / interpreter for OOP API \ No newline at end of file +- Score explainer / interpreter for OOP API +- Reimplement GreyNet in Rust \ No newline at end of file diff --git a/examples/greynet/greynet_example_all_collectors.py b/examples/greynet/greynet_example_all_collectors.py index e9a210e..22d337c 100644 --- a/examples/greynet/greynet_example_all_collectors.py +++ b/examples/greynet/greynet_example_all_collectors.py @@ -164,61 +164,6 @@ def filtering_collector_example(): .penalize_simple(lambda product_id, count: 0) # Reporting only ) -@cb.constraint("find_consecutive_shipments") -def consecutive_sequences_collector_example(): - """Demonstrates: consecutive_sequences - Finds consecutive sequences of shipment numbers for each order. - """ - return (cb.for_each(Shipment) - .group_by( - lambda s: s.order_id, - Collectors.consecutive_sequences(lambda s: s.shipment_no) - ) - .filter(lambda order_id, sequences: any(seq.length > 1 for seq in sequences)) - .penalize_simple(lambda order_id, sequences: 0) # Reporting only - ) - -@cb.constraint("find_overlapping_maintenance") -def connected_ranges_collector_example(): - """Demonstrates: connected_ranges - Finds groups of overlapping or adjacent maintenance windows for each machine. - """ - return (cb.for_each(Maintenance) - .group_by( - lambda m: m.machine_id, - Collectors.connected_ranges( - start_func=lambda m: m.start_time, - end_func=lambda m: m.end_time - ) - ) - .filter(lambda machine_id, ranges: any(len(r.data) > 1 for r in ranges)) - .penalize_simple(lambda machine_id, ranges: 0) # Reporting only - ) - -@cb.constraint("tumbling_window_events") -def tumbling_window_example(): - """Demonstrates: TumblingWindowCollector for aggregation - Groups events into 10-second, non-overlapping ("tumbling") windows - and calculates the average transaction value for each window. - """ - # Define a key function to map timestamps to a 10-second window start time - def get_window_key(timestamp: datetime) -> datetime: - epoch = datetime(1970, 1, 1, tzinfo=timezone.utc) - window_size_sec = 10 - elapsed_sec = (timestamp - epoch).total_seconds() - window_index = int(elapsed_sec // window_size_sec) - window_start_ts = epoch.timestamp() + window_index * window_size_sec - return datetime.fromtimestamp(window_start_ts, tz=timezone.utc) - - return (cb.for_each(UserEvent) - .group_by( - group_key_function=lambda e: get_window_key(e.timestamp), - collector_supplier=Collectors.avg(lambda e: e.value) - ) - .filter(lambda window_start, avg_value: avg_value > 0) - .penalize_simple(lambda window_start, avg_value: 0) # Reporting only - ) - # 3. Main Execution Block # ======================= diff --git a/examples/greynet/greynet_example_core.py b/examples/greynet/greynet_example_core.py index 5bafa78..dd042a0 100644 --- a/examples/greynet/greynet_example_core.py +++ b/examples/greynet/greynet_example_core.py @@ -102,34 +102,6 @@ def overlapping_shifts(): .penalize_hard(1.0) # Use new penalty method ) -# --- Rule 5: Maximum Consecutive Work Days (Soft Priority) --- -@builder.constraint("Exceeds max consecutive work days") -def max_consecutive_days(): - # This penalizes based on the number of days over the limit. - penalty_func = lambda name, sequences, policy: sum( - seq.length - policy.max_consecutive_work_days - for seq in sequences if seq.length > policy.max_consecutive_work_days - ) - - return ( - builder.for_each(Shift) - .group_by( - lambda shift: shift.employee_name, - Collectors.consecutive_sequences( - sequence_func=lambda shift: shift.shift_date, - increment_func=lambda d, i: d + timedelta(days=i) - ) - ) - .join( - builder.for_each(CompanyPolicy), - JoinerType.GREATER_THAN, - left_key_func=lambda name, sequences: 1, - right_key_func=lambda policy: 0 - ) - .filter(lambda name, sequences, policy: any(seq.length > policy.max_consecutive_work_days for seq in sequences)) - .penalize_soft(lambda name, sequences, policy: penalty_func(name, sequences, policy) * 50) # Dynamic penalty - ) - # --- 3. Execution and Verification (Updated for new score object) --- diff --git a/examples/greynet/greynet_example_temporal_1.py b/examples/greynet/greynet_example_temporal_1.py deleted file mode 100644 index b961d6a..0000000 --- a/examples/greynet/greynet_example_temporal_1.py +++ /dev/null @@ -1,225 +0,0 @@ -import uuid -from dataclasses import dataclass -from datetime import datetime, timedelta, timezone -from collections import deque -from greyjack.score_calculation.scores.SimpleScore import SimpleScore -from greyjack.score_calculation.greynet.greynet_fact import * -from greyjack.score_calculation.greynet.builder import ConstraintBuilder, Collectors -from greyjack.score_calculation.greynet.common.joiner_type import JoinerType - - -# TODO: Fix temporal, sequential features -# TODO: Add debug, tracing for temporal, sequential features - - -@greynet_fact -@dataclass -class UserEvent: - user_id: str - timestamp: datetime - -@greynet_fact -@dataclass -class FundDeposit(UserEvent): - amount: float - currency: str - -@greynet_fact -@dataclass -class TradeOrder(UserEvent): - amount: float - asset_pair: str - order_type: str - -@greynet_fact -@dataclass -class FundWithdrawal(UserEvent): - amount: float - to_address: str - -@greynet_fact -@dataclass -class AccountFlag(UserEvent): - reason: str - expires_at: datetime - - -def find_deposit_withdrawal_sequences(user_id, events): - pattern_steps = [ - lambda fact: isinstance(fact, FundDeposit) and fact.amount >= 10000, - lambda fact: isinstance(fact, FundWithdrawal) and fact.amount >= 9000 - ] - within_delta = timedelta(hours=24) - sorted_events = sorted(events, key=lambda e: e.timestamp) - - for i in range(len(sorted_events)): - if pattern_steps[0](sorted_events[i]): - for j in range(i + 1, len(sorted_events)): - event1 = sorted_events[i] - event2 = sorted_events[j] - if event2.timestamp - event1.timestamp > within_delta: - break - if pattern_steps[1](event2): - yield [event1, event2] - -def find_high_velocity_windows_for_user(withdrawals: list): - """ - Applies a continuous sliding window to a single user's sorted withdrawals - to find periods of high activity. - """ - if not withdrawals: - return - - window_size = timedelta(hours=1) - threshold = 100000 - - sorted_w = sorted(withdrawals, key=lambda w: w.timestamp) - - window = deque() - current_sum = 0.0 - yielded_violations = set() - - for w in sorted_w: - window.append(w) - current_sum += w.amount - - while w.timestamp - window[0].timestamp > window_size: - removed_fact = window.popleft() - current_sum -= removed_fact.amount - - if current_sum > threshold: - violation_key = frozenset(window) - if violation_key not in yielded_violations: - yield list(window) - yielded_violations.add(violation_key) - - -class FraudDetection: - def __init__(self): - self.builder = ConstraintBuilder(name="FraudDetectionSystem", score_class=SimpleScore) - self.define_constraints() - - def define_constraints(self): - """Uses the constraint decorator to define all detection rules.""" - - # Rule 1: High-Frequency Trading - @self.builder.constraint("HIGH_FREQ_TRADING", default_weight=1.0) - def high_frequency_trading(): - five_min_bucket = lambda ts: int(ts.timestamp() // 300) - return (self.builder.for_each(TradeOrder) - .group_by( - group_key_function=lambda trade: (trade.user_id, five_min_bucket(trade.timestamp)), - collector_supplier=Collectors.to_list() - ) - .filter(lambda group_key, trades: len(trades) > 50) - .penalize_simple(lambda group_key, trades: (len(trades) - 50) * 10) - ) - - # Rule 2: Suspicious Deposit -> Withdrawal Sequence - @self.builder.constraint("SUSPICIOUS_SEQUENCE", default_weight=1.0) - def suspicious_deposit_withdrawal_sequence(): - return (self.builder.for_each(UserEvent) - .group_by( - group_key_function=lambda event: event.user_id, - collector_supplier=Collectors.to_list() - ) - .flat_map(lambda user_id, events: find_deposit_withdrawal_sequences(user_id, events)) - .penalize_simple(5000) - ) - - @self.builder.constraint("HIGH_VELOCITY_WITHDRAWALS", default_weight=1.0) - def high_velocity_withdrawals(): - return (self.builder.for_each(FundWithdrawal) - .group_by( - group_key_function=lambda w: w.user_id, - collector_supplier=Collectors.to_list() - ) - .flat_map(lambda user_id, withdrawals: find_high_velocity_windows_for_user(withdrawals)) - .penalize_simple(lambda facts: sum(w.amount for w in facts) - 100000) - ) - - # Rule 4: Trading While Under Account Flag (Unchanged) - @self.builder.constraint("TRADING_WHILE_FLAGGED", default_weight=1.0) - def trading_while_flagged(): - high_value_trades = self.builder.for_each(TradeOrder).filter(lambda t: t.amount > 20000) - account_flags = self.builder.for_each(AccountFlag) - return (high_value_trades - .join(account_flags, - JoinerType.EQUAL, - left_key_func=lambda trade: trade.user_id, - right_key_func=lambda flag: flag.user_id - ) - .filter(lambda trade, flag: flag.timestamp <= trade.timestamp <= flag.expires_at) - .penalize_simple(1500) - ) - - def get_session(self): - return self.builder.build() - - -def run_simulation(): - print("### Setting up Fraud Detection System with Corrected Logic ###") - fraud_system = FraudDetection() - session = fraud_system.get_session() - - user_A = "user-Alice-123" - user_B = "user-Bob-456" - user_C = "user-Charlie-789" - - start_time = datetime.now(timezone.utc) - events = [] - - print("\n -> Generating data for [TRADING_WHILE_FLAGGED]") - events.append(AccountFlag(user_id=user_A, timestamp=start_time, reason="KYC_REVIEW", expires_at=start_time + timedelta(days=7))) - events.append(TradeOrder(user_id=user_A, timestamp=start_time + timedelta(hours=1), amount=25000, asset_pair="BTC/USD", order_type="SELL")) - - print(" -> Generating data for [SUSPICIOUS_SEQUENCE]") - events.append(FundDeposit(user_id=user_B, timestamp=start_time + timedelta(minutes=10), amount=15000, currency="USDC")) - events.append(FundWithdrawal(user_id=user_B, timestamp=start_time + timedelta(hours=5), amount=14950, to_address="0xabc...def")) - - print(" -> Generating data for [HIGH_FREQ_TRADING]") - for i in range(55): - events.append(TradeOrder(user_id=user_C, timestamp=start_time + timedelta(seconds=i*2), amount=100, asset_pair="ETH/USD", order_type="BUY")) - - print(" -> Generating data for [HIGH_VELOCITY_WITHDRAWALS]") - events.append(FundWithdrawal(user_id=user_A, timestamp=start_time + timedelta(minutes=30), amount=60000, to_address="0x123...456")) - events.append(FundWithdrawal(user_id=user_B, timestamp=start_time + timedelta(minutes=45), amount=45000, to_address="0x789...012")) - - print("\n### Inserting Events into Session ###") - session.insert_batch(events) - - print("\n### Calculating Initial Score ###") - print("Expected Score: 50 (HFT) + 10000 (2x Sequence) + 0 (High Velocity) + 1500 (Flagged) = 11550") - initial_score = session.get_score() - print(f"Actual Initial Total Score: {initial_score.simple_value}") - - print("\n### Retrieving Constraint Matches ###") - matches = session.get_constraint_matches() - - print("\n```mermaid") - print("graph TD") - print(" subgraph Detected Violations") - if not matches: - print(" No Violations Detected") - for constraint_id, violations in matches.items(): - print(f" {constraint_id} -- has {len(violations)} violation(s) --> V_{constraint_id}") - for i, violation in enumerate(violations): - score_object, _ = violation - score_val = score_object.simple_value - fact_summary = f"Penalty: {score_val:.2f}" - print(f" V_{constraint_id} -- \"{fact_summary}\" --> V_{constraint_id}_{i}") - print(" end") - print("```") - - print("\n### Demonstrating Dynamic Weight Update ###") - print(" -> Increasing weight of 'TRADING_WHILE_FLAGGED' from 1.0 to 3.0") - session.update_constraint_weight("TRADING_WHILE_FLAGGED", 3.0) - - updated_score = session.get_score() - print(f"Expected Updated Score: 11550 - 1500 + (1500 * 3) = 14550") - print(f"Actual Updated Total Score: {updated_score.simple_value}") - print(f"Score increased by: {updated_score.simple_value - initial_score.simple_value:.2f}") - - -if __name__ == "__main__": - run_simulation() diff --git a/examples/greynet/greynet_example_temporal_2.py b/examples/greynet/greynet_example_temporal_2.py deleted file mode 100644 index a51cc89..0000000 --- a/examples/greynet/greynet_example_temporal_2.py +++ /dev/null @@ -1,177 +0,0 @@ -from dataclasses import dataclass -from datetime import datetime, timedelta -from typing import Union - -# Assuming all provided files are in a structured 'greyjack' directory -from greyjack.score_calculation.greynet.greynet_fact import * -from greyjack.score_calculation.greynet.builder import ConstraintBuilder, Collectors -from greyjack.score_calculation.greynet.common.joiner_type import JoinerType -from greyjack.score_calculation.scores.HardSoftScore import HardSoftScore - -# TODO: Fix temporal, sequential features -# TODO: Add debug, tracing for temporal, sequential features - -# --- Data Models --- - -@greynet_fact -@dataclass() -class LoginAttempt: - user: str - ip_address: str - timestamp: datetime - successful: bool - -@greynet_fact -@dataclass() -class FileAccess: - user: str - file_path: str - operation: str # 'read', 'write', 'delete' - timestamp: datetime - -@greynet_fact -@dataclass() -class NetworkConnection: - user: str - dest_ip: str - dest_port: int - timestamp: datetime - -@greynet_fact -@dataclass() -class AdminUser: - username: str - -# A Union type for convenience when starting the stream -SystemEvent = Union[LoginAttempt, FileAccess, NetworkConnection] - -def define_security_constraints(builder: ConstraintBuilder): - """ - Defines the sequence detection constraint. - - Pattern: - 1. A failed login attempt. - 2. Within 5 mins, a successful login for the SAME user from a DIFFERENT IP. - 3. Within 10 mins of success, the user accesses a sensitive file in /etc/. - 4. Within 2 mins of file access, the user makes an outbound connection on a known suspicious port (6667). - 5. The rule only triggers if the user is NOT a registered administrator. - """ - - # Helper predicate to validate the logic of the detected sequence - def validate_complex_sequence(sequence: list[SystemEvent]) -> bool: - # sequence[0] = Failed Login - # sequence[1] = Successful Login - # sequence[2] = File Access - # sequence[3] = Network Connection - - failed_login, successful_login, file_access, network_conn = sequence - - # Condition 1: Same user throughout the sequence - user = failed_login.user - if not (user == successful_login.user == file_access.user == network_conn.user): - return False - - # Condition 2: Successful login from a different IP - if failed_login.ip_address == successful_login.ip_address: - return False - - # Condition 3: Check intra-sequence time gaps - if not (successful_login.timestamp - failed_login.timestamp <= timedelta(minutes=5)): - return False - if not (file_access.timestamp - successful_login.timestamp <= timedelta(minutes=10)): - return False - if not (network_conn.timestamp - file_access.timestamp <= timedelta(minutes=2)): - return False - - return True - - @builder.constraint("anomalous_access_and_exfiltration", default_weight=1.0) - def detect_attack_pattern(): - - # The sequence of event types we are looking for - pattern_steps = [ - lambda e: isinstance(e, LoginAttempt) and not e.successful, - lambda e: isinstance(e, LoginAttempt) and e.successful, - lambda e: isinstance(e, FileAccess) and e.file_path.startswith('/etc/'), - lambda e: isinstance(e, NetworkConnection) and e.dest_port == 6667, - ] - - return ( - builder.for_each(SystemEvent) - # --- Start of Bug Fix --- - .sequence( - lambda e: e.timestamp, # time_extractor is the first argument - *pattern_steps, # Unpack steps as positional arguments - within=timedelta(minutes=15)# The entire sequence must complete within 15 mins - ) - # --- End of Bug Fix --- - # The stream now contains UniTuples where fact_a is a list of the 4 events - .filter(lambda seq: validate_complex_sequence(seq)) - - # Transform the stream of [event_list] into a stream of [username] - .flat_map(lambda seq: [seq[0].user]) - - # Now, check if this user exists in the AdminUser fact source. - # Only propagate the username if they DO NOT exist in the admin list. - .if_not_exists( - AdminUser, - left_key=lambda user_fact: user_fact, # The username from flat_map - right_key=lambda admin_fact: admin_fact.username - ) - # The stream now contains only usernames of non-admins who triggered the pattern. - .penalize_hard(lambda user: 100) # Assign a penalty of 100 for each detected user. - ) - -def run_simulation(): - # --- 1. Setup --- - builder = ConstraintBuilder(name="SecurityRules", score_class=HardSoftScore) - define_security_constraints(builder) - session = builder.build() - - # --- 2. Test Data --- - base_time = datetime(2025, 7, 15, 12, 0, 0) - - # Scenario 1: Malicious user 'intruder' - # This sequence should trigger the rule. - attack_sequence = [ - LoginAttempt(user='intruder', ip_address='1.1.1.1', timestamp=base_time, successful=False), - LoginAttempt(user='intruder', ip_address='2.2.2.2', timestamp=base_time + timedelta(minutes=1), successful=True), - FileAccess(user='intruder', file_path='/etc/shadow', operation='read', timestamp=base_time + timedelta(minutes=3),), - NetworkConnection(user='intruder', dest_ip='3.3.3.3', dest_port=6667, timestamp=base_time + timedelta(minutes=4)), - ] - - # Scenario 2: Admin user 's_admin' performs similar actions - # This should NOT trigger the rule due to the 'if_not_exists' check. - admin_actions = [ - LoginAttempt(user='s_admin', ip_address='10.0.0.1', timestamp=base_time + timedelta(hours=1), successful=False), - LoginAttempt(user='s_admin', ip_address='10.0.0.2', timestamp=base_time + timedelta(hours=1, minutes=1), successful=True), - FileAccess(user='s_admin', file_path='/etc/hosts', operation='write', timestamp=base_time + timedelta(hours=1, minutes=2)), - NetworkConnection(user='s_admin', dest_ip='8.8.8.8', dest_port=6667, timestamp=base_time + timedelta(hours=1, minutes=3)), - ] - - # --- 3. Execution --- - session.insert_batch([AdminUser(username='s_admin')]) # Add the admin to the session - session.insert_batch(attack_sequence) - session.insert_batch(admin_actions) - - score = session.get_score() - matches = session.get_constraint_matches() - - # --- 4. Display Results --- - print("## Simulation Results\n") - print(f"**Final Score:** {score}\n") - - if "anomalous_access_and_exfiltration" in matches: - print("**Detected Violations for 'anomalous_access_and_exfiltration':**\n") - - for score_obj, tuple_obj in matches["anomalous_access_and_exfiltration"]: - offending_user = tuple_obj.fact_a - print(f"- **User:** `{offending_user}`") - print(f"- **Penalty:** `{score_obj.simple_value}`") - print(" - **Reason:** This user, who is not an administrator, performed a sequence of actions matching the attack pattern.") - else: - print("**No violations detected.**") - - -if __name__ == "__main__": - run_simulation() diff --git a/examples/object_oriented/cloud_balancing/scripts/solve_cloud_balancing.py b/examples/object_oriented/cloud_balancing/scripts/solve_cloud_balancing.py index f023e6a..c65ce71 100644 --- a/examples/object_oriented/cloud_balancing/scripts/solve_cloud_balancing.py +++ b/examples/object_oriented/cloud_balancing/scripts/solve_cloud_balancing.py @@ -30,7 +30,7 @@ domain_builder = DomainBuilder(file_path) - cotwin_builder = CotwinBuilder(scorer_name="pseudo", use_greed_init=True) + cotwin_builder = CotwinBuilder(scorer_name="greynet_incremental", use_greed_init=True) #termination_strategy = StepsLimit(step_count_limit=1000) #termination_strategy = TimeSpentLimit(time_seconds_limit=60) diff --git a/examples/object_oriented/nqueens/scripts/solve_nqueens_greynet_experimental.py b/examples/object_oriented/nqueens/scripts/solve_nqueens_greynet_experimental.py index ef48c3a..7431985 100644 --- a/examples/object_oriented/nqueens/scripts/solve_nqueens_greynet_experimental.py +++ b/examples/object_oriented/nqueens/scripts/solve_nqueens_greynet_experimental.py @@ -34,7 +34,7 @@ termination_strategy = ScoreLimit(score_to_compare=[0]) agent = TabuSearch(neighbours_count=20, tabu_entity_rate=0.0, mutation_rate_multiplier=None, move_probas=[0, 1, 0, 0, 0, 0], - migration_frequency=9999999999999, termination_strategy=termination_strategy) + migration_frequency=999_999_999, termination_strategy=termination_strategy) """agent = GeneticAlgorithm(population_size=128, crossover_probability=0.5, p_best_rate=0.05, tabu_entity_rate=0.0, mutation_rate_multiplier=1.0, move_probas=[0, 1, 0, 0, 0, 0], migration_rate=0.00001, migration_frequency=1, termination_strategy=termination_strategy)""" diff --git a/greyjack/Cargo.lock b/greyjack/Cargo.lock index d4784b1..7648f52 100644 --- a/greyjack/Cargo.lock +++ b/greyjack/Cargo.lock @@ -579,7 +579,7 @@ checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" [[package]] name = "greyjack" -version = "0.3.5" +version = "0.3.6" dependencies = [ "chrono", "ndarray", diff --git a/greyjack/Cargo.toml b/greyjack/Cargo.toml index a67e41d..c0a5c53 100644 --- a/greyjack/Cargo.toml +++ b/greyjack/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "greyjack" -version = "0.3.5" +version = "0.3.6" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/greyjack/README.md b/greyjack/README.md index b2d6b60..dbd0cd2 100644 --- a/greyjack/README.md +++ b/greyjack/README.md @@ -29,9 +29,9 @@ There are 2 editions of GreyJack Solver: ``` pip install greyjack ``` - -- Explore examples. Docs and guides will be later. GreyJack is very intuitively understandable solver (even Rust version). -- Simply solve your tasks simply. +- Clone data for examples from this [repo](https://github.com/CameleoGrey/greyjack-data-for-examples) +- Explore, try examples. Docs and guides will be later. GreyJack is very intuitively understandable solver (even Rust version). +- Use examples as reference for solving your tasks. # Install GreyJack Solver from source @@ -46,11 +46,11 @@ maturin develop --release # RoadMap - Types, arguments validation - Write docs -- Tests, tests, tests... +- Tests, tests, tests... + integration wtih CI/CD - Composite termination criterion (for example: solving limit minutes N AND score not improving M seconds) - Multi-level score - Custom moves support -- Try to impove incremental (pseudo-incremental) score calculation mechanism (caching, no clonning, etc) - Website - Useful text materials, guides, presentations -- Score explainer / interpreter for OOP API \ No newline at end of file +- Score explainer / interpreter for OOP API +- Reimplement GreyNet in Rust \ No newline at end of file diff --git a/greyjack/greyjack/agents/base/Agent.py b/greyjack/greyjack/agents/base/Agent.py index 1f84ffe..88ece71 100644 --- a/greyjack/greyjack/agents/base/Agent.py +++ b/greyjack/greyjack/agents/base/Agent.py @@ -240,10 +240,6 @@ def _step_plain(self): candidates = [self.individual_type(samples[i].copy(), scores[i]) for i in range(len(samples))] new_population = self.metaheuristic_base.build_updated_population(self.population, candidates) - - #if self.score_requester.is_greynet: - # self.score_requester.cotwin.score_calculator.update_entity_mapping_plain(new_population[0].variable_values) - self.population = new_population def _step_incremental(self): @@ -256,29 +252,7 @@ def _step_incremental(self): new_population, new_values = self.metaheuristic_base.build_updated_population_incremental(self.population, sample, deltas, scores) if self.score_requester.is_greynet and new_values is not None: - - ################## self.score_requester.cotwin.score_calculator.commit_deltas(new_values) - - #self.score_requester.cotwin.score_calculator._apply_deltas_internal(new_values) - #self.score_requester.cotwin.score_calculator.update_entity_mapping_incremental(new_values) - #new_score = self.score_requester.cotwin.score_calculator.get_score() - #new_population[0] = self.individual_type(new_population[0].variable_values, new_score) - ################## - - ################## - # gives correct results, but lacks of performance due to linear updates for each acceptable solution - #self.score_requester.cotwin.score_calculator._apply_deltas_internal(list(enumerate(new_population[0].variable_values))) - #new_score = self.score_requester.cotwin.score_calculator.get_score() - #new_population[0] = self.individual_type(new_population[0].variable_values, new_score) - ################## - - #new_score = self.score_requester.cotwin.score_calculator._full_sync_and_get_score(new_population[0].variable_values) - #new_population[0] = self.individual_type(new_population[0].variable_values, new_score) - - #self.score_requester.cotwin.score_calculator._apply_deltas_internal(new_values) - #new_score = self.score_requester.cotwin.score_calculator.get_score() - #new_population[0] = self.individual_type(new_population[0].variable_values, new_score) self.population = new_population @@ -314,7 +288,6 @@ def _send_updates_universal(self): break else: continue - # population already sorted after step #self.population.sort() @@ -330,13 +303,6 @@ def _send_updates_universal(self): "round_robin_status_dict": self.round_robin_status_dict, "request_type": "put_updates", "migrants": migrants} - """if self.metaheuristic_base.metaheuristic_name == "LSHADE": - if len(self.history_archive) > 0: - rand_id = random.randint(0, len(self.history_archive) - 1) - request["history_archive"] = self.history_archive[rand_id].as_list() - #request["history_archive"] = self.history_archive[-1] - else: - request["history_archive"] = None""" request_serialized = pickle.dumps(request) try: @@ -371,15 +337,6 @@ def _get_updates_universal(self): self.agent_to_agent_socket_receiver.send(pickle.dumps("Successfully received updates")) updates_reply = pickle.loads( updates_reply ) - """if self.metaheuristic_base.metaheuristic_name == "LSHADE": - history_migrant = updates_reply["history_archive"] - if (history_migrant is not None and len(self.history_archive) > 0): - history_migrant = self.individual_type.from_list(history_migrant) - rand_id = random.randint(0, len(self.history_archive) - 1) - #if updates_reply["history_archive"] < self.history_archive[-1]: - if history_migrant < self.history_archive[rand_id]: - self.history_archive[rand_id] = history_migrant""" - migrants = updates_reply["migrants"] migrants = self.individual_type.convert_lists_to_individuals(migrants) n_migrants = len(migrants) @@ -431,13 +388,6 @@ def _send_updates_linux(self): "round_robin_status_dict": self.round_robin_status_dict, "request_type": "put_updates", "migrants": migrants} - """if self.metaheuristic_base.metaheuristic_name == "LSHADE": - if len(self.history_archive) > 0: - rand_id = random.randint(0, len(self.history_archive) - 1) - request["history_archive"] = self.history_archive[rand_id].as_list() - #request["history_archive"] = self.history_archive[-1] - else: - request["history_archive"] = None""" try: self.agent_to_agent_pipe_sender.send( request ) @@ -459,15 +409,6 @@ def _get_updates_linux(self): return self.agent_to_agent_pipe_receiver.send("Successfully received updates") - """if self.metaheuristic_base.metaheuristic_name == "LSHADE": - history_migrant = updates_reply["history_archive"] - if (history_migrant is not None and len(self.history_archive) > 0): - history_migrant = self.individual_type.from_list(history_migrant) - rand_id = random.randint(0, len(self.history_archive) - 1) - #if updates_reply["history_archive"] < self.history_archive[-1]: - if history_migrant < self.history_archive[rand_id]: - self.history_archive[rand_id] = history_migrant""" - migrants = updates_reply["migrants"] migrants = self.individual_type.convert_lists_to_individuals(migrants) n_migrants = len(migrants) @@ -485,11 +426,6 @@ def _get_updates_linux(self): self.population[:n_migrants] = updated_tail else: raise Exception("metaheuristic_kind can be only Population or LocalSearch") - - #if self.score_requester.is_greynet: - # self.score_requester.cotwin.score_calculator._full_sync_and_get_score(new_population[0].variable_values) - #if self.score_requester.is_greynet: - # self.score_requester.cotwin.score_calculator.update_entity_mapping_plain(self.population[0].variable_values) self.round_robin_status_dict = updates_reply["round_robin_status_dict"] self.round_robin_status_dict[self.agent_id] = self.agent_status @@ -517,8 +453,6 @@ def _send_candidate_to_master_universal(self, step_id): def _check_global_updates(self): self._check_global_updates_universal() - - def _check_global_updates_universal(self): master_publication = self.agent_to_master_subscriber_socket.recv() @@ -530,10 +464,6 @@ def _check_global_updates_universal(self): if global_top_individual < self.agent_top_individual: self.agent_top_individual = global_top_individual self.population[0] = global_top_individual.copy() - #if self.score_requester.is_greynet: - # self.score_requester.cotwin.score_calculator._full_sync_and_get_score(new_population[0].variable_values) - #if self.score_requester.is_greynet: - # self.score_requester.cotwin.score_calculator.update_entity_mapping_plain(self.population[0].variable_values) is_variable_names_received = master_publication[1] self.is_master_received_variables_info = is_variable_names_received diff --git a/greyjack/greyjack/score_calculation/greynet/builder.py b/greyjack/greyjack/score_calculation/greynet/builder.py index 42ac7ba..099f1ff 100644 --- a/greyjack/greyjack/score_calculation/greynet/builder.py +++ b/greyjack/greyjack/score_calculation/greynet/builder.py @@ -28,10 +28,6 @@ from .collectors.mapping_collector import MappingCollector from .collectors.filtering_collector import FilteringCollector from .collectors.constraint_match_collector import ConstraintMatchCollector - - -from .constraint_tools.consecutive_set_tree import ConsecutiveSetTree -from .constraint_tools.connected_range_tracker import ConnectedRangeTracker from .constraint_tools.counting_bloom_filter import CountingBloomFilter @@ -220,30 +216,6 @@ def is_empty(self): return len(self.bf) == 0 return BloomCollector - @staticmethod - def consecutive_sequences(sequence_func, increment_func=lambda p, i: p + i): - """ - Creates a collector that groups items into consecutive sequences. - """ - class Collector: - def __init__(self): self.tree = ConsecutiveSetTree(sequence_func, increment_func) - def insert(self, item): self.tree.add(item); return lambda: self.tree.remove(item) - def result(self): return self.tree.get_sequences() - def is_empty(self): return not self.tree.get_sequences() - return Collector - - @staticmethod - def connected_ranges(start_func, end_func): - """ - Creates a collector that groups items into connected (overlapping or adjacent) ranges. - """ - class Collector: - def __init__(self): self.tracker = ConnectedRangeTracker(start_func, end_func) - def insert(self, item): self.tracker.add(item); return lambda: self.tracker.remove(item) - def result(self): return self.tracker.get_connected_ranges() - def is_empty(self): return not self.tracker.get_connected_ranges() - return Collector - class Patterns: """A helper class for defining common, complex constraint patterns.""" diff --git a/greyjack/greyjack/score_calculation/greynet/collectors/temporal_collectors.py b/greyjack/greyjack/score_calculation/greynet/collectors/temporal_collectors.py deleted file mode 100644 index 6a31b3a..0000000 --- a/greyjack/greyjack/score_calculation/greynet/collectors/temporal_collectors.py +++ /dev/null @@ -1,180 +0,0 @@ -from collections import defaultdict, deque -from datetime import datetime, timedelta, timezone -# --- Start of Bug Fix --- -from typing import Dict, List, Callable, Optional, Any, Tuple -# --- End of Bug Fix --- -from dataclasses import dataclass -from typing import Optional -from enum import Enum -import bisect - -# TODO: Fix temporal, sequential features -# TODO: Add debug, tracing for temporal, sequential features - -class WindowType(Enum): - TUMBLING = "tumbling" # Non-overlapping fixed windows - SLIDING = "sliding" # Overlapping windows that slide - SESSION = "session" # Dynamic windows based on activity gaps - HOPPING = "hopping" # Fixed-size windows with custom hop interval - -@dataclass(frozen=True) -class TimeWindow: - start: datetime - end: datetime - window_type: WindowType - - def contains(self, timestamp: datetime) -> bool: - return self.start <= timestamp < self.end - - def overlaps(self, other: 'TimeWindow') -> bool: - return not (self.end <= other.start or other.end <= self.start) - - def duration(self) -> timedelta: - return self.end - self.start - - @classmethod - def tumbling(cls, start: datetime, duration: timedelta) -> 'TimeWindow': - return cls(start, start + duration, WindowType.TUMBLING) - - @classmethod - def sliding(cls, start: datetime, duration: timedelta) -> 'TimeWindow': - return cls(start, start + duration, WindowType.SLIDING) - -@dataclass -class TemporalEvent: - """Wrapper for facts with temporal information""" - fact: Any - timestamp: datetime - event_id: Optional[str] = None - - def __post_init__(self): - if self.event_id is None: - self.event_id = f"evt_{self.fact.greynet_fact_id}_{self.timestamp.timestamp()}" - -# --- Start of Bug Fix --- -@dataclass() -class EventSequencePattern: - """ - ENHANCED: Improved pattern definition with better validation. - """ - pattern_steps: Tuple[Callable[[Any], bool], ...] - within: timedelta - allow_gaps: bool = True -# --- End of Bug Fix --- - - def __post_init__(self): - """Validate pattern configuration.""" - if not self.pattern_steps: - raise ValueError("Pattern must have at least one step") - - if len(self.pattern_steps) < 2: - raise ValueError("Pattern must have at least two steps to form a sequence") - - if self.within.total_seconds() <= 0: - raise ValueError("Time window must be positive") - - # Validate that all steps are callable - for i, step in enumerate(self.pattern_steps): - if not callable(step): - raise ValueError(f"Pattern step {i} must be callable") - - def matches_sequence(self, facts: List[Any], timestamps: List[datetime]) -> bool: - """ - ENHANCED: Validates that a sequence of facts matches this pattern. - """ - if len(facts) != len(self.pattern_steps): - return False - - if len(facts) != len(timestamps): - return False - - # Check time window constraint - if timestamps[-1] - timestamps[0] > self.within: - return False - - # Check each step matches - for i, (fact, predicate) in enumerate(zip(facts, self.pattern_steps)): - if not predicate(fact): - return False - - # Check temporal ordering (timestamps should be non-decreasing) - for i in range(1, len(timestamps)): - if timestamps[i] < timestamps[i-1]: - return False - - # If gaps are not allowed, check for strict temporal adjacency - if not self.allow_gaps: - # This would require domain-specific logic to determine - # what constitutes "adjacent" events - pass - - return True - -class TemporalCollector: - """Base class for temporal aggregation collectors""" - - def __init__(self, time_extractor: Callable[[Any], datetime]): - self.time_extractor = time_extractor - self.events: List[TemporalEvent] = [] - - def insert(self, item: Any): - timestamp = self.time_extractor(item) - event = TemporalEvent(item, timestamp) - - insert_pos = bisect.bisect_left(self.events, event.timestamp, key=lambda e: e.timestamp) - self.events.insert(insert_pos, event) - - def undo(): - try: - self.events.remove(event) - except ValueError: - pass - return undo - - def get_events_in_window(self, window: TimeWindow) -> List[Any]: - start_idx = bisect.bisect_left(self.events, window.start, key=lambda e: e.timestamp) - end_idx = bisect.bisect_right(self.events, window.end, key=lambda e: e.timestamp) - return [e.fact for e in self.events[start_idx:end_idx]] - -class TumblingWindowCollector(TemporalCollector): - """Collector that creates non-overlapping tumbling windows""" - - def __init__(self, - time_extractor: Callable[[Any], datetime], - window_size: timedelta, - window_start: Optional[datetime] = None): - super().__init__(time_extractor) - self.window_size = window_size - - if window_start: - self.window_start_epoch = window_start.timestamp() - else: - self.window_start_epoch = datetime(1970, 1, 1, tzinfo=timezone.utc).timestamp() - - self._windows: Dict[int, List[Any]] = defaultdict(list) - - def _get_window_key(self, timestamp: datetime) -> int: - elapsed = timestamp.timestamp() - self.window_start_epoch - window_size_sec = self.window_size.total_seconds() - window_index = int(elapsed // window_size_sec) - return int(self.window_start_epoch + window_index * window_size_sec) - - def insert(self, item: Any): - undo_super = super().insert(item) - self._rebuild_windows() - def undo(): - undo_super() - self._rebuild_windows() - return undo - - def _rebuild_windows(self): - self._windows.clear() - for event in self.events: - window_key = self._get_window_key(event.timestamp) - self._windows[window_key].append(event.fact) - - def result(self) -> Dict[datetime, List[Any]]: - return {datetime.fromtimestamp(k, tz=timezone.utc): v for k, v in self._windows.items()} - - def is_empty(self) -> bool: - return not self.events diff --git a/greyjack/greyjack/score_calculation/greynet/constraint_tools/connected_range_tracker.py b/greyjack/greyjack/score_calculation/greynet/constraint_tools/connected_range_tracker.py deleted file mode 100644 index ece81fc..0000000 --- a/greyjack/greyjack/score_calculation/greynet/constraint_tools/connected_range_tracker.py +++ /dev/null @@ -1,105 +0,0 @@ - -# greynet/constraint_tools/connected_range_tracker.py - -from dataclasses import dataclass -from typing import Any, List - -@dataclass(order=True) -class ConnectedRange: - start: Any - end: Any - data: List[Any] - - def can_connect(self, other): - # Ranges connect if they overlap or are immediately adjacent. - return not (self.end < other.start or other.end < self.start) - - def merge(self, other): - return ConnectedRange( - start=min(self.start, other.start), - end=max(self.end, other.end), - data=self.data + other.data - ) - -class ConnectedRangeTracker: - def __init__(self, start_mapping, end_mapping): - self._start_mapping = start_mapping - self._end_mapping = end_mapping - self._ranges = [] - self._item_to_range = {} - - def add(self, item): - start, end = self._start_mapping(item), self._end_mapping(item) - new_range = ConnectedRange(start=start, end=end, data=[item]) - - overlapping, remaining = [], [] - for r in self._ranges: - if new_range.can_connect(r): - overlapping.append(r) - else: - remaining.append(r) - - merged = new_range - for r in overlapping: - merged = merged.merge(r) - - self._ranges = remaining # Temporarily remove merged ranges - self._insert_sorted(merged) # Add the new merged range - - # Update the item-to-range mapping for all items in the new range - for item_in_merged in merged.data: - self._item_to_range[item_in_merged] = merged - - def remove(self, item): - if item not in self._item_to_range: - return - - containing_range = self._item_to_range.pop(item) - self._ranges.remove(containing_range) - - remaining_items = [i for i in containing_range.data if i != item] - if not remaining_items: - return - - # Rebuild ranges from the remaining items, as the removal might - # have split a single continuous range into two or more. - new_ranges = self._rebuild_ranges(remaining_items) - for r in new_ranges: - self._insert_sorted(r) - for i in r.data: - self._item_to_range[i] = r - - def get_connected_ranges(self): - return self._ranges.copy() - - def _insert_sorted(self, range_): - import bisect - bisect.insort_left(self._ranges, range_, key=lambda r: r.start) - - def _rebuild_ranges(self, items): - if not items: - return [] - # Sort items by their start time to reconstruct ranges efficiently - sorted_items = sorted(items, key=self._start_mapping) - - rebuilt_ranges = [] - current_range = ConnectedRange( - self._start_mapping(sorted_items[0]), - self._end_mapping(sorted_items[0]), - [sorted_items[0]] - ) - - for item in sorted_items[1:]: - item_range = ConnectedRange( - self._start_mapping(item), - self._end_mapping(item), - [item] - ) - if current_range.can_connect(item_range): - current_range = current_range.merge(item_range) - else: - rebuilt_ranges.append(current_range) - current_range = item_range - - rebuilt_ranges.append(current_range) - return rebuilt_ranges diff --git a/greyjack/greyjack/score_calculation/greynet/constraint_tools/consecutive_set_tree.py b/greyjack/greyjack/score_calculation/greynet/constraint_tools/consecutive_set_tree.py deleted file mode 100644 index f019fc9..0000000 --- a/greyjack/greyjack/score_calculation/greynet/constraint_tools/consecutive_set_tree.py +++ /dev/null @@ -1,134 +0,0 @@ - -# greynet/constraint_tools/consecutive_set_tree.py - -from dataclasses import dataclass -from typing import Any, Tuple - -@dataclass(frozen=True, order=True) -class ConsecutiveSequence: - """Represents a continuous sequence of items.""" - start: Any - end: Any - length: int - items: Tuple[Any, ...] - -class ConsecutiveSetTree: - """ - A data structure for efficiently tracking consecutive sequences of items. - """ - def __init__(self, sequence_function, increment_function): - self._sequence_func = sequence_function - self._inc_func = increment_function - self._item_to_pos = {} - self._pos_to_seq = {} - self._sequences = set() - - def add(self, item): - """Adds an item and updates the sequences accordingly.""" - if item in self._item_to_pos: - return - - pos = self._sequence_func(item) - self._item_to_pos[item] = pos - - prev_pos = self._inc_func(pos, -1) - next_pos = self._inc_func(pos, 1) - - prev_seq = self._pos_to_seq.get(prev_pos) - next_seq = self._pos_to_seq.get(next_pos) - - if prev_seq and next_seq: - if prev_seq is not next_seq: - self._merge(prev_seq, next_seq, item, pos) - elif prev_seq: - self._extend(prev_seq, item, pos) - elif next_seq: - self._prepend(next_seq, item, pos) - else: - self._create(item, pos) - - def remove(self, item): - """ - Robustly removes an item by finding its sequence, tearing it down, - and rebuilding new sequences from the remaining items. This approach - correctly handles cases where a sequence is split in two. - """ - if item not in self._item_to_pos: - return - - pos = self._item_to_pos[item] # Get position without popping - seq = self._pos_to_seq.get(pos) - - if not seq: - # The item was mapped but not part of a sequence. Just clean up its own mappings. - del self._item_to_pos[item] - if self._pos_to_seq.get(pos) is None: - del self._pos_to_seq[pos] - return - - # 1. Identify which items we need to re-add later. - items_to_re_add = [i for i in seq.items if i is not item] - - # 2. Completely remove the old sequence and all its associated mappings - # to ensure a clean state before rebuilding. - self._sequences.discard(seq) - for i in seq.items: - if i in self._item_to_pos: - item_pos = self._item_to_pos.pop(i) - if self._pos_to_seq.get(item_pos) is seq: - del self._pos_to_seq[item_pos] - - # Clean boundary pointers as a safeguard. - if self._pos_to_seq.get(seq.start) is seq: - del self._pos_to_seq[seq.start] - if self._pos_to_seq.get(seq.end) is seq: - del self._pos_to_seq[seq.end] - - # 3. Re-add the remaining items. The `add` method will correctly form - # new sequences (e.g., splitting the old one into two). - for i in items_to_re_add: - self.add(i) - - def _update_mappings(self, seq): - """Updates internal dictionaries to map positions to the new sequence.""" - for item in seq.items: - pos = self._item_to_pos.get(item) - if pos is not None: - self._pos_to_seq[pos] = seq - self._pos_to_seq[seq.start] = seq - self._pos_to_seq[seq.end] = seq - - def _create(self, item, pos): - """Creates a new sequence of length 1.""" - seq = ConsecutiveSequence(start=pos, end=pos, length=1, items=(item,)) - self._sequences.add(seq) - self._update_mappings(seq) - - def _extend(self, seq, item, pos): - """Adds an item to the end of an existing sequence.""" - self._sequences.remove(seq) - new_items = seq.items + (item,) - new_seq = ConsecutiveSequence(start=seq.start, end=pos, length=len(new_items), items=new_items) - self._sequences.add(new_seq) - self._update_mappings(new_seq) - - def _prepend(self, seq, item, pos): - """Adds an item to the beginning of an existing sequence.""" - self._sequences.remove(seq) - new_items = (item,) + seq.items - new_seq = ConsecutiveSequence(start=pos, end=seq.end, length=len(new_items), items=new_items) - self._sequences.add(new_seq) - self._update_mappings(new_seq) - - def _merge(self, prev_seq, next_seq, item, pos): - """Merges two sequences with a new item in between.""" - self._sequences.remove(prev_seq) - self._sequences.remove(next_seq) - new_items = prev_seq.items + (item,) + next_seq.items - new_seq = ConsecutiveSequence(start=prev_seq.start, end=next_seq.end, length=len(new_items), items=new_items) - self._sequences.add(new_seq) - self._update_mappings(new_seq) - - def get_sequences(self): - """Returns the current list of all consecutive sequences.""" - return list(self._sequences) diff --git a/greyjack/greyjack/score_calculation/greynet/nodes/sequence_pattern_node.py b/greyjack/greyjack/score_calculation/greynet/nodes/sequence_pattern_node.py deleted file mode 100644 index d948601..0000000 --- a/greyjack/greyjack/score_calculation/greynet/nodes/sequence_pattern_node.py +++ /dev/null @@ -1,164 +0,0 @@ - -# greynet/nodes/sequence_pattern_node.py - -from __future__ import annotations -import bisect -from collections import defaultdict -from datetime import datetime -from typing import Callable, Any, List - -from ..nodes.abstract_node import AbstractNode -from ..core.tuple import UniTuple, TupleState, AbstractTuple -from ..core.tuple_pool import TuplePool -from ..collectors.temporal_collectors import EventSequencePattern - -class SequencePatternNode(AbstractNode): - """ - A node that correctly detects sequences of facts matching a defined pattern. - - This version contains a corrected algorithm for sequence detection that properly - handles event insertion and retraction by rescanning for all valid sequences - and reconciling the engine's state. - """ - def __init__(self, node_id: int, pattern: EventSequencePattern, - time_extractor: Callable[[Any], datetime], scheduler, tuple_pool: TuplePool): - super().__init__(node_id) - self._pattern = pattern - self._time_extractor = time_extractor - self._scheduler = scheduler - self._tuple_pool = tuple_pool - - # State Management - # A sorted list of all events (facts) that could be part of a sequence. - self._events: List[tuple[datetime, AbstractTuple]] = [] - # Tracks active sequences to manage propagation and retraction. - # Key: frozenset of parent tuple IDs. Value: The propagated child tuple. - self._active_sequences: dict[frozenset[int], UniTuple] = {} - - def insert(self, parent_tuple: AbstractTuple): - """Inserts a fact, adds it to the event timeline, and re-evaluates sequences.""" - timestamp = self._time_extractor(parent_tuple.fact_a) - event_entry = (timestamp, parent_tuple) - - # Insert event while maintaining chronological order. - bisect.insort_left(self._events, event_entry) - self._rescan_and_reconcile() - - def retract(self, parent_tuple: AbstractTuple): - """Retracts a fact, removes it from the timeline, and re-evaluates sequences.""" - timestamp = self._time_extractor(parent_tuple.fact_a) - event_entry = (timestamp, parent_tuple) - - try: - self._events.remove(event_entry) - self._rescan_and_reconcile() - except ValueError: - # Fact was not in the event list, nothing to do. - return - - def _rescan_and_reconcile(self): - """ - Scans for all currently valid sequences and reconciles the engine state - by propagating new sequences and retracting those that are no longer valid. - """ - all_found_sequences = self._find_all_valid_sequences() - - current_keys = set(self._active_sequences.keys()) - found_keys = set(all_found_sequences.keys()) - - # Retract sequences that were active but are no longer valid. - for key in current_keys - found_keys: - child_tuple = self._active_sequences.pop(key) - self._retract_child(child_tuple) - - # Propagate new valid sequences that were not previously active. - for key in found_keys - current_keys: - parent_tuples = all_found_sequences[key] - child_tuple = self._propagate_new_sequence(parent_tuples) - self._active_sequences[key] = child_tuple - - def _find_all_valid_sequences(self) -> dict[frozenset[int], List[AbstractTuple]]: - """ - Finds all unique, complete sequences that match the pattern from the current event list. - """ - found_sequences = {} - - # Iterate through all events, treating each as a potential start of a sequence. - for i, (start_time, start_tuple) in enumerate(self._events): - # The event must match the first step of the pattern. - if self._pattern.pattern_steps[0](start_tuple.fact_a): - # Find all possible complete sequences starting from this event. - initial_sequence = [start_tuple] - complete_sequences = self._find_sequence_completions( - current_sequence=initial_sequence, - search_from_index=i + 1, - sequence_start_time=start_time - ) - - for sequence in complete_sequences: - # Use a key based on the object IDs of the facts to uniquely - # identify this specific instance of a sequence. - key = frozenset(t.greynet_fact_id for t in sequence) - if key not in found_sequences: - found_sequences[key] = sequence - - return found_sequences - - def _find_sequence_completions(self, current_sequence: List[AbstractTuple], - search_from_index: int, - sequence_start_time: datetime) -> List[List[AbstractTuple]]: - """ - A robust recursive function to find all valid completions of a partial sequence. - """ - # Base case: The sequence has the required number of steps. It's a valid completion. - if len(current_sequence) == len(self._pattern.pattern_steps): - return [current_sequence] - - next_step_index = len(current_sequence) - next_predicate = self._pattern.pattern_steps[next_step_index] - all_completions = [] - - # Iterate through subsequent events to find a match for the next step. - for i in range(search_from_index, len(self._events)): - event_time, event_tuple = self._events[i] - - # Optimization: Stop searching if the event is outside the pattern's time window. - if event_time - sequence_start_time > self._pattern.within: - break - - # Check if this event matches the predicate for the next step in the pattern. - if next_predicate(event_tuple.fact_a): - # We found a potential next event. - extended_sequence = current_sequence + [event_tuple] - - # Recursively find the rest of the sequence, starting from the event AFTER this one. - completions = self._find_sequence_completions( - extended_sequence, - i + 1, - sequence_start_time - ) - all_completions.extend(completions) - - # If gaps are not allowed, we MUST use this first match we found. - # We cannot continue searching for other candidates for this same step. - if not self._pattern.allow_gaps: - break - - return all_completions - - def _propagate_new_sequence(self, parent_tuples: List[AbstractTuple]) -> UniTuple: - """Creates and schedules a new child tuple representing a valid sequence.""" - # The fact of the child tuple is the list of facts from the sequence. - sequence_facts = [p.fact_a for p in parent_tuples] - child_tuple = self._tuple_pool.acquire(UniTuple, fact_a=sequence_facts) - child_tuple.node, child_tuple.state = self, TupleState.CREATING - self._scheduler.schedule(child_tuple) - return child_tuple - - def _retract_child(self, child_tuple: UniTuple): - """Schedules a child tuple for retraction if it's no longer valid.""" - if child_tuple.state == TupleState.CREATING: - child_tuple.state = TupleState.ABORTING - elif not child_tuple.state.is_dirty(): - child_tuple.state = TupleState.DYING - self._scheduler.schedule(child_tuple) diff --git a/greyjack/greyjack/score_calculation/greynet/nodes/sliding_window_node.py b/greyjack/greyjack/score_calculation/greynet/nodes/sliding_window_node.py deleted file mode 100644 index c71c556..0000000 --- a/greyjack/greyjack/score_calculation/greynet/nodes/sliding_window_node.py +++ /dev/null @@ -1,120 +0,0 @@ -# greynet/nodes/sliding_window_node.py - -from __future__ import annotations -import bisect -from collections import defaultdict -from datetime import datetime, timedelta, timezone - -from .abstract_node import AbstractNode -from ..core.tuple import BiTuple, TupleState, AbstractTuple -from ..core.tuple_pool import TuplePool - -class SlidingWindowNode(AbstractNode): - """ - A node that groups facts into sliding time windows. - - For each window, it emits a BiTuple where: - - fact_a: The start datetime of the window. - - fact_b: A list of all facts that fall within that window. - - The node correctly handles insertion and retraction of facts, updating - the corresponding window tuples as needed. - """ - def __init__(self, node_id: int, time_extractor: callable, window_size: timedelta, - slide_interval: timedelta, scheduler, tuple_pool: TuplePool): - super().__init__(node_id) - self._time_extractor = time_extractor - self._window_size = window_size - self._slide_interval = slide_interval - self._scheduler = scheduler - self._tuple_pool = tuple_pool - - # State management - self._events: list[tuple[datetime, AbstractTuple]] = [] - self._fact_to_windows = defaultdict(set) # Maps fact.id -> {window_start_ts, ...} - self._active_windows = {} # Maps window_start_ts -> emitted_BiTuple - - # Use a fixed epoch for reproducible window alignment - self._epoch_start_ts = datetime(1970, 1, 1, tzinfo=timezone.utc).timestamp() - self._slide_sec = self._slide_interval.total_seconds() - self._window_sec = self._window_size.total_seconds() - - def _get_windows_for_timestamp(self, ts: datetime) -> list[datetime]: - """Calculates all sliding windows a given timestamp falls into.""" - ts_seconds = ts.timestamp() - # First possible window starts such that its end (start + size) includes the timestamp - first_window_start_idx = (ts_seconds - self._window_sec) / self._slide_sec - # Last possible window starts such that its start is before or at the timestamp - last_window_start_idx = ts_seconds / self._slide_sec - - windows = [] - # Iterate through all possible window start indices - for i in range(int(first_window_start_idx) + 1, int(last_window_start_idx) + 1): - start_ts = self._epoch_start_ts + i * self._slide_sec - windows.append(datetime.fromtimestamp(start_ts, tz=timezone.utc)) - return windows - - def insert(self, tuple_): - fact = tuple_.fact_a - fact_id = fact.id - timestamp = self._time_extractor(fact) - - # Maintain a sorted list of events - event_entry = (timestamp, fact) - bisect.insort(self._events, event_entry) - - affected_windows = self._get_windows_for_timestamp(timestamp) - for window_start in affected_windows: - self._fact_to_windows[fact_id].add(window_start) - self._update_window(window_start) - - def retract(self, tuple_): - fact = tuple_.fact_a - fact_id = fact.id - timestamp = self._time_extractor(fact) - - event_entry = (timestamp, fact) - try: - # O(N) removal, can be optimized if needed - self._events.remove(event_entry) - except ValueError: - return # Fact was not present - - # Update all windows this fact was part of - if fact_id in self._fact_to_windows: - affected_windows = self._fact_to_windows.pop(fact_id) - for window_start in affected_windows: - self._update_window(window_start) - - def _update_window(self, window_start: datetime): - """(Re)calculates and propagates a single window.""" - window_end = window_start + self._window_size - - # Find all facts within this window's time range from the sorted list - start_idx = bisect.bisect_left(self._events, (window_start, None)) - end_idx = bisect.bisect_right(self._events, (window_end, None)) - - facts_in_window = [fact for ts, fact in self._events[start_idx:end_idx]] - - # Retract the old tuple for this window if it exists - if window_start in self._active_windows: - old_tuple = self._active_windows.pop(window_start) - self._retract_child(old_tuple) - - # If there are facts, create and propagate a new tuple - if facts_in_window: - new_tuple = self._create_child(window_start, facts_in_window) - self._active_windows[window_start] = new_tuple - - def _create_child(self, key: datetime, result: list) -> BiTuple: - tuple_ = self._tuple_pool.acquire(BiTuple, fact_a=key, fact_b=result) - tuple_.node, tuple_.state = self, TupleState.CREATING - self._scheduler.schedule(tuple_) - return tuple_ - - def _retract_child(self, tuple_: BiTuple): - if tuple_.state == TupleState.CREATING: - tuple_.state = TupleState.ABORTING - elif not tuple_.state.is_dirty(): - tuple_.state = TupleState.DYING - self._scheduler.schedule(tuple_) diff --git a/greyjack/greyjack/score_calculation/greynet/streams/stream.py b/greyjack/greyjack/score_calculation/greynet/streams/stream.py index b67c07b..e324825 100644 --- a/greyjack/greyjack/score_calculation/greynet/streams/stream.py +++ b/greyjack/greyjack/score_calculation/greynet/streams/stream.py @@ -7,10 +7,8 @@ from .stream_definition import ( FilterDefinition, JoinDefinition, GroupByDefinition, ConditionalJoinDefinition, FlatMapDefinition, - SlidingWindowDefinition, SequencePatternDefinition ) from ..core.tuple import UniTuple, BiTuple, AbstractTuple -from ..collectors.temporal_collectors import EventSequencePattern if TYPE_CHECKING: from ..constraint_factory import ConstraintFactory @@ -107,35 +105,6 @@ def build_node(self, node_counter, node_map, scheduler, tuple_pool): """Builds the Rete node for this stream and wires its children.""" node = self.definition.build_node(node_counter, node_map, scheduler, tuple_pool) return node - - # --- Temporal and Sequential Extensions --- - - def sequence(self, time_extractor: Callable[[Any], datetime], *steps: Callable[[Any], bool], - within: timedelta, allow_gaps: bool = True) -> Stream['UniTuple']: - """ - Finds sequences of facts that match a series of predicates within a time window. - The stream emits a list of the facts that form a complete sequence. - """ - if self.arity != 1: - raise TypeError("The .sequence() operation can only be applied to a stream of single facts (UniTuple).") - - # --- Start of Bug Fix --- - # `steps` is already a tuple. Using `list(steps)` made the pattern unhashable. - pattern = EventSequencePattern(pattern_steps=steps, within=within, allow_gaps=allow_gaps) - # --- End of Bug Fix --- - seq_def = SequencePatternDefinition(self.constraint_factory, self, pattern, time_extractor) - new_stream = Stream[UniTuple](self.constraint_factory, seq_def) - self._add_next_stream(new_stream) - return new_stream - - def window(self, time_extractor: Callable[[Any], datetime]) -> 'WindowedStream': - """ - Initiates a windowing operation on the stream. Must be followed by a window type - like .sliding() or .tumbling(). - """ - if self.arity != 1: - raise TypeError("Windowing operations can only be applied to a stream of single facts (UniTuple).") - return WindowedStream(self, time_extractor) # --- Penalty Methods --- def _create_penalty(self, score_type: str, penalty: Union[int, float, Callable]) -> Constraint: @@ -158,36 +127,4 @@ def penalize_medium(self, penalty: Union[int, float, Callable]) -> Constraint: return self._create_penalty("medium", penalty) def penalize_simple(self, penalty: Union[int, float, Callable]) -> Constraint: - return self._create_penalty("simple", penalty) - - -class WindowedStream: - """A helper class to provide a fluent API for creating time windows.""" - def __init__(self, source_stream: Stream, time_extractor: Callable[[Any], datetime]): - self._source_stream = source_stream - self._time_extractor = time_extractor - - def sliding(self, size: timedelta, slide: timedelta) -> Stream['BiTuple']: - """ - Groups facts into overlapping windows of a fixed size that advance at a specified interval. - Emits a stream of (window_start_time, [facts_in_window]) BiTuples. - """ - if slide.total_seconds() <= 0 or size.total_seconds() <= 0: - raise ValueError("Window size and slide interval must be positive.") - if slide > size: - raise ValueError("Slide interval cannot be greater than the window size for a sliding window.") - - window_def = SlidingWindowDefinition( - self._source_stream.constraint_factory, self._source_stream, - self._time_extractor, size, slide - ) - new_stream = Stream[BiTuple](self._source_stream.constraint_factory, window_def) - self._source_stream._add_next_stream(new_stream) - return new_stream - - def tumbling(self, size: timedelta) -> Stream['BiTuple']: - """ - Groups facts into non-overlapping windows of a fixed size. - This is a special case of a sliding window where the slide interval equals the size. - """ - return self.sliding(size=size, slide=size) + return self._create_penalty("simple", penalty) \ No newline at end of file diff --git a/greyjack/greyjack/score_calculation/greynet/streams/stream_definition.py b/greyjack/greyjack/score_calculation/greynet/streams/stream_definition.py index 312e0f4..5b9ead3 100644 --- a/greyjack/greyjack/score_calculation/greynet/streams/stream_definition.py +++ b/greyjack/greyjack/score_calculation/greynet/streams/stream_definition.py @@ -11,8 +11,6 @@ from ..nodes.from_uni_node import FromUniNode from ..nodes.flatmap_node import FlatMapNode from ..nodes.conditional_node import ConditionalNode -from ..nodes.sliding_window_node import SlidingWindowNode -from ..nodes.sequence_pattern_node import SequencePatternNode from .join_adapters import JoinLeftAdapter, JoinRightAdapter from ..function import Function @@ -22,7 +20,6 @@ from ..core.tuple_pool import TuplePool from ..core.tuple import AbstractTuple from ..nodes.abstract_node import AbstractNode - from ..collectors.temporal_collectors import EventSequencePattern class StreamDefinition(ABC): """An abstract base class for defining the behavior of a stream node.""" @@ -224,58 +221,4 @@ def apply(self, parent_tuple: 'AbstractTuple'): final_mapper_obj = FlatMapWrapper(self.mapper) - return FlatMapNode(node_id, final_mapper_obj, scheduler, tuple_pool) - -class SlidingWindowDefinition(StreamDefinition): - """Definition for a sliding window operation.""" - def __init__(self, factory: 'ConstraintFactory', source_stream: 'Stream', - time_extractor: Callable[[Any], datetime], window_size: timedelta, slide_interval: timedelta): - super().__init__(factory, source_stream) - self.time_extractor = time_extractor - self.window_size = window_size - self.slide_interval = slide_interval - self.retrieval_id = ('sliding_window', source_stream.definition.retrieval_id, - time_extractor, window_size, slide_interval) - - def get_target_arity(self) -> int: return 2 - - def build_node(self, node_counter, node_map, scheduler, tuple_pool: 'TuplePool') -> 'AbstractNode': - node = node_map.get(self.retrieval_id) - if node is None: - node = self._create_node(node_counter, scheduler, tuple_pool) - parent_node = self.source_stream.definition.build_node(node_counter, node_map, scheduler, tuple_pool) - parent_node.add_child_node(node) - node_map[self.retrieval_id] = node - return node - - def _create_node(self, node_counter, scheduler, tuple_pool) -> 'AbstractNode': - node_id = node_counter.value; node_counter.value += 1 - return SlidingWindowNode( - node_id, self.time_extractor, self.window_size, self.slide_interval, scheduler, tuple_pool - ) - -class SequencePatternDefinition(StreamDefinition): - """Definition for a sequence detection operation.""" - def __init__(self, factory: 'ConstraintFactory', source_stream: 'Stream', - pattern: 'EventSequencePattern', time_extractor: Callable[[Any], datetime]): - super().__init__(factory, source_stream) - self.pattern = pattern - self.time_extractor = time_extractor - self.retrieval_id = ('sequence', source_stream.definition.retrieval_id, pattern, time_extractor) - - def get_target_arity(self) -> int: return 1 - - def build_node(self, node_counter, node_map, scheduler, tuple_pool: 'TuplePool') -> 'AbstractNode': - node = node_map.get(self.retrieval_id) - if node is None: - node = self._create_node(node_counter, scheduler, tuple_pool) - parent_node = self.source_stream.definition.build_node(node_counter, node_map, scheduler, tuple_pool) - parent_node.add_child_node(node) - node_map[self.retrieval_id] = node - return node - - def _create_node(self, node_counter, scheduler, tuple_pool) -> 'AbstractNode': - node_id = node_counter.value; node_counter.value += 1 - return SequencePatternNode( - node_id, self.pattern, self.time_extractor, scheduler, tuple_pool - ) + return FlatMapNode(node_id, final_mapper_obj, scheduler, tuple_pool) \ No newline at end of file diff --git a/greyjack/greyjack/score_calculation/score_calculators/GreynetScoreCalculator.py b/greyjack/greyjack/score_calculation/score_calculators/GreynetScoreCalculator.py index 02f5f9a..6fb9bf6 100644 --- a/greyjack/greyjack/score_calculation/score_calculators/GreynetScoreCalculator.py +++ b/greyjack/greyjack/score_calculation/score_calculators/GreynetScoreCalculator.py @@ -74,7 +74,6 @@ def get_score(self) -> Any: Returns: A score object (e.g., HardSoftScore) representing the current state. """ - #self.session.recalculate_all_scores() score = self.session.get_score() return score @@ -106,33 +105,6 @@ def _apply_and_get_score_for_batch(self, deltas: List[List[Tuple[int, float]]]) return scores - def map_deltas_to_entities(self, deltas: List[Tuple[int, float]]) -> List[Any]: - - entity_objects: List[Any] = [] - - for var_idx, new_value in deltas: - entity, attr_name = self.var_idx_to_entity_map[var_idx] - mapped_entity = deepcopy(entity) - setattr(mapped_entity, attr_name, new_value) - entity_objects.append(mapped_entity) - - return entity_objects - - def update_entity_mapping_plain(self, sample): - delta_updates = list(enumerate(sample)) - - for var_idx, new_value in delta_updates: - entity, attr_name = self.var_idx_to_entity_map[var_idx] - setattr(entity, attr_name, new_value) - - self._apply_deltas_internal(delta_updates) - - def update_entity_mapping_incremental(self, deltas): - - for var_idx, new_value in deltas: - entity, attr_name = self.var_idx_to_entity_map[var_idx] - setattr(entity, attr_name, new_value) - def _apply_deltas_internal(self, deltas: List[Tuple[int, float]]) -> Tuple[List[Any], List[Any]]: """ @@ -153,23 +125,10 @@ def _apply_deltas_internal(self, deltas: List[Tuple[int, float]]) -> Tuple[List[ originals = list(original_to_changed_map.keys()) changed = list(original_to_changed_map.values()) - #pprint(self.session.get_constraint_matches()) - if originals: - #print_internals_from_entity_mapping(self.var_idx_to_entity_map) - #print_internals(originals, "row_id") - #score_1 = self.get_score() self.session.retract_batch(originals) - #self.session.flush() - #score_2 = self.get_score() - - #print_internals(changed, "row_id") self.session.insert_batch(changed) self.session.flush() - #score_3 = self.get_score() - - #print() - #self.session.recalculate_all_scores() return originals, changed @@ -179,19 +138,10 @@ def _revert_deltas_internal(self, originals: List[Any], changed: List[Any]): It retracts the modified copies and re-inserts the original facts. """ if changed: - - #score_1 = self.get_score() self.session.retract_batch(changed) - #self.session.flush() - #score_2 = self.get_score() - self.session.insert_batch(originals) self.session.flush() - #score_3 = self.get_score() - - #print() pass - #self.session.recalculate_all_scores() def commit_deltas(self, deltas): @@ -209,11 +159,6 @@ def commit_deltas(self, deltas): originals = list(original_to_changed_map.keys()) changed = list(original_to_changed_map.values()) - #for i in range(len(originals)): - # tmp = changed[i].greynet_fact_id - # changed[i].greynet_fact_id = originals[i].greynet_fact_id - # originals[i].greynet_fact_id = tmp - if originals: self.session.retract_batch(originals) #self.session.flush() @@ -224,28 +169,4 @@ def commit_deltas(self, deltas): for var_idx, (entity, attr_name) in self.var_idx_to_entity_map.items(): original_id = id(entity) if original_id in original_id_to_new_entity_map: - self.var_idx_to_entity_map[var_idx] = (original_id_to_new_entity_map[original_id], attr_name) - - #self.session.recalculate_all_scores() - -# for debug -@staticmethod -def print_internals(entities, attr_name): - - values = [] - for entity in entities: - value = getattr(entity, attr_name) - values.append(value) - - print(values) - -@staticmethod -def print_internals_from_entity_mapping(var_idx_to_entity_map): - - values = [] - for var_idx in var_idx_to_entity_map.keys(): - entity, attr_name = var_idx_to_entity_map[var_idx] - value = getattr(entity, attr_name) - values.append(value) - - print(values) + self.var_idx_to_entity_map[var_idx] = (original_id_to_new_entity_map[original_id], attr_name) \ No newline at end of file diff --git a/greyjack/greyjack/score_calculation/score_requesters/OOPScoreRequester.py b/greyjack/greyjack/score_calculation/score_requesters/OOPScoreRequester.py index b0b5ae2..7674019 100644 --- a/greyjack/greyjack/score_calculation/score_requesters/OOPScoreRequester.py +++ b/greyjack/greyjack/score_calculation/score_requesters/OOPScoreRequester.py @@ -11,13 +11,12 @@ class OOPScoreRequester: def __init__(self, cotwin): self.cotwin = cotwin - self.is_greynet = isinstance(self.cotwin.score_calculator, GreynetScoreCalculator) - - # This initialization logic is common to both modes variables_vec, var_name_to_vec_id_map, vec_id_to_var_name_map = self.build_variables_info(self.cotwin) self.variables_manager = VariablesManagerPy(variables_vec) self.vec_id_to_var_name_map = vec_id_to_var_name_map + self.is_greynet = isinstance(self.cotwin.score_calculator, GreynetScoreCalculator) + if self.is_greynet: self._init_greynet() else: @@ -46,7 +45,6 @@ def _init_greynet(self): except: print(traceback.format_exc()) - # Perform the initial full load of the Greynet session calculator.initial_load(initialized_planning_entities, self.cotwin.problem_facts) def build_initialized_entities(self, planning_entities, group_name): @@ -72,9 +70,6 @@ def build_initialized_entity(self, entity): value = attribute_value.planning_variable.initial_value if value is None: raise ValueError("All planning variables must have initial value for scoring by greynet") - - #if type(attribute_value) in {GJInteger, GJBinary}: - # value = int(value) else: value = attribute_value @@ -85,10 +80,6 @@ def build_initialized_entity(self, entity): new_entity.greynet_fact_id = entity.greynet_fact_id return new_entity - - def set_correct_greynet_state(self, chosen_deltas): - calculator = self.cotwin.score_calculator - calculator._apply_deltas_internal(chosen_deltas) def _init_plain(self, variables_vec, var_name_to_vec_id_map, vec_id_to_var_name_map): """Initializes the requester for the standard DataFrame-based calculation.""" @@ -110,25 +101,19 @@ def _init_plain(self, variables_vec, var_name_to_vec_id_map, vec_id_to_var_name_ def request_score_plain(self, samples): if self.is_greynet: - # Delegate to the calculator's full sync method for each sample return [self.cotwin.score_calculator._full_sync_and_get_score(s) for s in samples] else: - # Use the existing Rust-based DataFrame builder planning_entity_dfs, problem_fact_dfs = self.candidate_dfs_builder.get_plain_candidate_dfs(samples) return self.cotwin.get_score_plain(planning_entity_dfs, problem_fact_dfs) def request_score_incremental(self, sample, deltas): if self.is_greynet: - # Delegate the entire batch of deltas to the calculator return self.cotwin.score_calculator._apply_and_get_score_for_batch(deltas) else: - # Use the existing Rust-based incremental logic planning_entity_dfs, problem_fact_dfs, delta_dfs_for_rust = self.candidate_dfs_builder.get_incremental_candidate_dfs(sample, deltas) return self.cotwin.get_score_incremental(planning_entity_dfs, problem_fact_dfs, delta_dfs_for_rust) - # The following methods are shared helpers and remain unchanged. def build_variables_info(self, cotwin): - # ... (implementation is identical to the one provided) variables_vec = [] var_name_to_vec_id_map = {} vec_id_to_var_name_map = {} @@ -150,7 +135,6 @@ def build_variables_info(self, cotwin): return variables_vec, var_name_to_vec_id_map, vec_id_to_var_name_map def build_column_map(self, entity_groups): - # ... (implementation is identical to the one provided) column_dict = {} entity_is_int_map = {} for group_name in entity_groups: @@ -168,7 +152,6 @@ def build_column_map(self, entity_groups): return column_dict, entity_is_int_map def build_group_dfs(self, entity_groups, column_dict, is_planning): - # ... (implementation is identical to the one provided) df_dict = {} for df_name in column_dict: column_names = column_dict[df_name] diff --git a/greyjack/pyproject.toml b/greyjack/pyproject.toml index 04d3fe6..d90d9e9 100644 --- a/greyjack/pyproject.toml +++ b/greyjack/pyproject.toml @@ -7,12 +7,11 @@ features = ["pyo3/extension-module"] [project] name = "greyjack" -version = "0.3.5" +version = "0.3.6" requires-python = ">=3.9" dependencies = [ "bitarray>=3.5.0", "dill", - "matplotlib>=3.9.4", "maturin>=1.9.1", "mmh3>=5.1.0", "multiprocess", diff --git a/greyjack/uv.lock b/greyjack/uv.lock index 40499d9..ff14587 100644 --- a/greyjack/uv.lock +++ b/greyjack/uv.lock @@ -166,164 +166,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8c/52/b08750ce0bce45c143e1b5d7357ee8c55341b52bdef4b0f081af1eb248c2/cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662", size = 181290 }, ] -[[package]] -name = "contourpy" -version = "1.3.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10'", -] -dependencies = [ - { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f5/f6/31a8f28b4a2a4fa0e01085e542f3081ab0588eff8e589d39d775172c9792/contourpy-1.3.0.tar.gz", hash = "sha256:7ffa0db17717a8ffb127efd0c95a4362d996b892c2904db72428d5b52e1938a4", size = 13464370 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6c/e0/be8dcc796cfdd96708933e0e2da99ba4bb8f9b2caa9d560a50f3f09a65f3/contourpy-1.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:880ea32e5c774634f9fcd46504bf9f080a41ad855f4fef54f5380f5133d343c7", size = 265366 }, - { url = "https://files.pythonhosted.org/packages/50/d6/c953b400219443535d412fcbbc42e7a5e823291236bc0bb88936e3cc9317/contourpy-1.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:76c905ef940a4474a6289c71d53122a4f77766eef23c03cd57016ce19d0f7b42", size = 249226 }, - { url = "https://files.pythonhosted.org/packages/6f/b4/6fffdf213ffccc28483c524b9dad46bb78332851133b36ad354b856ddc7c/contourpy-1.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:92f8557cbb07415a4d6fa191f20fd9d2d9eb9c0b61d1b2f52a8926e43c6e9af7", size = 308460 }, - { url = "https://files.pythonhosted.org/packages/cf/6c/118fc917b4050f0afe07179a6dcbe4f3f4ec69b94f36c9e128c4af480fb8/contourpy-1.3.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:36f965570cff02b874773c49bfe85562b47030805d7d8360748f3eca570f4cab", size = 347623 }, - { url = "https://files.pythonhosted.org/packages/f9/a4/30ff110a81bfe3abf7b9673284d21ddce8cc1278f6f77393c91199da4c90/contourpy-1.3.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cacd81e2d4b6f89c9f8a5b69b86490152ff39afc58a95af002a398273e5ce589", size = 317761 }, - { url = "https://files.pythonhosted.org/packages/99/e6/d11966962b1aa515f5586d3907ad019f4b812c04e4546cc19ebf62b5178e/contourpy-1.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:69375194457ad0fad3a839b9e29aa0b0ed53bb54db1bfb6c3ae43d111c31ce41", size = 322015 }, - { url = "https://files.pythonhosted.org/packages/4d/e3/182383743751d22b7b59c3c753277b6aee3637049197624f333dac5b4c80/contourpy-1.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a52040312b1a858b5e31ef28c2e865376a386c60c0e248370bbea2d3f3b760d", size = 1262672 }, - { url = "https://files.pythonhosted.org/packages/78/53/974400c815b2e605f252c8fb9297e2204347d1755a5374354ee77b1ea259/contourpy-1.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3faeb2998e4fcb256542e8a926d08da08977f7f5e62cf733f3c211c2a5586223", size = 1321688 }, - { url = "https://files.pythonhosted.org/packages/52/29/99f849faed5593b2926a68a31882af98afbeac39c7fdf7de491d9c85ec6a/contourpy-1.3.0-cp310-cp310-win32.whl", hash = "sha256:36e0cff201bcb17a0a8ecc7f454fe078437fa6bda730e695a92f2d9932bd507f", size = 171145 }, - { url = "https://files.pythonhosted.org/packages/a9/97/3f89bba79ff6ff2b07a3cbc40aa693c360d5efa90d66e914f0ff03b95ec7/contourpy-1.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:87ddffef1dbe5e669b5c2440b643d3fdd8622a348fe1983fad7a0f0ccb1cd67b", size = 216019 }, - { url = "https://files.pythonhosted.org/packages/b3/1f/9375917786cb39270b0ee6634536c0e22abf225825602688990d8f5c6c19/contourpy-1.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0fa4c02abe6c446ba70d96ece336e621efa4aecae43eaa9b030ae5fb92b309ad", size = 266356 }, - { url = "https://files.pythonhosted.org/packages/05/46/9256dd162ea52790c127cb58cfc3b9e3413a6e3478917d1f811d420772ec/contourpy-1.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:834e0cfe17ba12f79963861e0f908556b2cedd52e1f75e6578801febcc6a9f49", size = 250915 }, - { url = "https://files.pythonhosted.org/packages/e1/5d/3056c167fa4486900dfbd7e26a2fdc2338dc58eee36d490a0ed3ddda5ded/contourpy-1.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dbc4c3217eee163fa3984fd1567632b48d6dfd29216da3ded3d7b844a8014a66", size = 310443 }, - { url = "https://files.pythonhosted.org/packages/ca/c2/1a612e475492e07f11c8e267ea5ec1ce0d89971be496c195e27afa97e14a/contourpy-1.3.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4865cd1d419e0c7a7bf6de1777b185eebdc51470800a9f42b9e9decf17762081", size = 348548 }, - { url = "https://files.pythonhosted.org/packages/45/cf/2c2fc6bb5874158277b4faf136847f0689e1b1a1f640a36d76d52e78907c/contourpy-1.3.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:303c252947ab4b14c08afeb52375b26781ccd6a5ccd81abcdfc1fafd14cf93c1", size = 319118 }, - { url = "https://files.pythonhosted.org/packages/03/33/003065374f38894cdf1040cef474ad0546368eea7e3a51d48b8a423961f8/contourpy-1.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:637f674226be46f6ba372fd29d9523dd977a291f66ab2a74fbeb5530bb3f445d", size = 323162 }, - { url = "https://files.pythonhosted.org/packages/42/80/e637326e85e4105a802e42959f56cff2cd39a6b5ef68d5d9aee3ea5f0e4c/contourpy-1.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:76a896b2f195b57db25d6b44e7e03f221d32fe318d03ede41f8b4d9ba1bff53c", size = 1265396 }, - { url = "https://files.pythonhosted.org/packages/7c/3b/8cbd6416ca1bbc0202b50f9c13b2e0b922b64be888f9d9ee88e6cfabfb51/contourpy-1.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e1fd23e9d01591bab45546c089ae89d926917a66dceb3abcf01f6105d927e2cb", size = 1324297 }, - { url = "https://files.pythonhosted.org/packages/4d/2c/021a7afaa52fe891f25535506cc861c30c3c4e5a1c1ce94215e04b293e72/contourpy-1.3.0-cp311-cp311-win32.whl", hash = "sha256:d402880b84df3bec6eab53cd0cf802cae6a2ef9537e70cf75e91618a3801c20c", size = 171808 }, - { url = "https://files.pythonhosted.org/packages/8d/2f/804f02ff30a7fae21f98198828d0857439ec4c91a96e20cf2d6c49372966/contourpy-1.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:6cb6cc968059db9c62cb35fbf70248f40994dfcd7aa10444bbf8b3faeb7c2d67", size = 217181 }, - { url = "https://files.pythonhosted.org/packages/c9/92/8e0bbfe6b70c0e2d3d81272b58c98ac69ff1a4329f18c73bd64824d8b12e/contourpy-1.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:570ef7cf892f0afbe5b2ee410c507ce12e15a5fa91017a0009f79f7d93a1268f", size = 267838 }, - { url = "https://files.pythonhosted.org/packages/e3/04/33351c5d5108460a8ce6d512307690b023f0cfcad5899499f5c83b9d63b1/contourpy-1.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:da84c537cb8b97d153e9fb208c221c45605f73147bd4cadd23bdae915042aad6", size = 251549 }, - { url = "https://files.pythonhosted.org/packages/51/3d/aa0fe6ae67e3ef9f178389e4caaaa68daf2f9024092aa3c6032e3d174670/contourpy-1.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0be4d8425bfa755e0fd76ee1e019636ccc7c29f77a7c86b4328a9eb6a26d0639", size = 303177 }, - { url = "https://files.pythonhosted.org/packages/56/c3/c85a7e3e0cab635575d3b657f9535443a6f5d20fac1a1911eaa4bbe1aceb/contourpy-1.3.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9c0da700bf58f6e0b65312d0a5e695179a71d0163957fa381bb3c1f72972537c", size = 341735 }, - { url = "https://files.pythonhosted.org/packages/dd/8d/20f7a211a7be966a53f474bc90b1a8202e9844b3f1ef85f3ae45a77151ee/contourpy-1.3.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eb8b141bb00fa977d9122636b16aa67d37fd40a3d8b52dd837e536d64b9a4d06", size = 314679 }, - { url = "https://files.pythonhosted.org/packages/6e/be/524e377567defac0e21a46e2a529652d165fed130a0d8a863219303cee18/contourpy-1.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3634b5385c6716c258d0419c46d05c8aa7dc8cb70326c9a4fb66b69ad2b52e09", size = 320549 }, - { url = "https://files.pythonhosted.org/packages/0f/96/fdb2552a172942d888915f3a6663812e9bc3d359d53dafd4289a0fb462f0/contourpy-1.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0dce35502151b6bd35027ac39ba6e5a44be13a68f55735c3612c568cac3805fd", size = 1263068 }, - { url = "https://files.pythonhosted.org/packages/2a/25/632eab595e3140adfa92f1322bf8915f68c932bac468e89eae9974cf1c00/contourpy-1.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:aea348f053c645100612b333adc5983d87be69acdc6d77d3169c090d3b01dc35", size = 1322833 }, - { url = "https://files.pythonhosted.org/packages/73/e3/69738782e315a1d26d29d71a550dbbe3eb6c653b028b150f70c1a5f4f229/contourpy-1.3.0-cp312-cp312-win32.whl", hash = "sha256:90f73a5116ad1ba7174341ef3ea5c3150ddf20b024b98fb0c3b29034752c8aeb", size = 172681 }, - { url = "https://files.pythonhosted.org/packages/0c/89/9830ba00d88e43d15e53d64931e66b8792b46eb25e2050a88fec4a0df3d5/contourpy-1.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:b11b39aea6be6764f84360fce6c82211a9db32a7c7de8fa6dd5397cf1d079c3b", size = 218283 }, - { url = "https://files.pythonhosted.org/packages/53/a1/d20415febfb2267af2d7f06338e82171824d08614084714fb2c1dac9901f/contourpy-1.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3e1c7fa44aaae40a2247e2e8e0627f4bea3dd257014764aa644f319a5f8600e3", size = 267879 }, - { url = "https://files.pythonhosted.org/packages/aa/45/5a28a3570ff6218d8bdfc291a272a20d2648104815f01f0177d103d985e1/contourpy-1.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:364174c2a76057feef647c802652f00953b575723062560498dc7930fc9b1cb7", size = 251573 }, - { url = "https://files.pythonhosted.org/packages/39/1c/d3f51540108e3affa84f095c8b04f0aa833bb797bc8baa218a952a98117d/contourpy-1.3.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32b238b3b3b649e09ce9aaf51f0c261d38644bdfa35cbaf7b263457850957a84", size = 303184 }, - { url = "https://files.pythonhosted.org/packages/00/56/1348a44fb6c3a558c1a3a0cd23d329d604c99d81bf5a4b58c6b71aab328f/contourpy-1.3.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d51fca85f9f7ad0b65b4b9fe800406d0d77017d7270d31ec3fb1cc07358fdea0", size = 340262 }, - { url = "https://files.pythonhosted.org/packages/2b/23/00d665ba67e1bb666152131da07e0f24c95c3632d7722caa97fb61470eca/contourpy-1.3.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:732896af21716b29ab3e988d4ce14bc5133733b85956316fb0c56355f398099b", size = 313806 }, - { url = "https://files.pythonhosted.org/packages/5a/42/3cf40f7040bb8362aea19af9a5fb7b32ce420f645dd1590edcee2c657cd5/contourpy-1.3.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d73f659398a0904e125280836ae6f88ba9b178b2fed6884f3b1f95b989d2c8da", size = 319710 }, - { url = "https://files.pythonhosted.org/packages/05/32/f3bfa3fc083b25e1a7ae09197f897476ee68e7386e10404bdf9aac7391f0/contourpy-1.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c6c7c2408b7048082932cf4e641fa3b8ca848259212f51c8c59c45aa7ac18f14", size = 1264107 }, - { url = "https://files.pythonhosted.org/packages/1c/1e/1019d34473a736664f2439542b890b2dc4c6245f5c0d8cdfc0ccc2cab80c/contourpy-1.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f317576606de89da6b7e0861cf6061f6146ead3528acabff9236458a6ba467f8", size = 1322458 }, - { url = "https://files.pythonhosted.org/packages/22/85/4f8bfd83972cf8909a4d36d16b177f7b8bdd942178ea4bf877d4a380a91c/contourpy-1.3.0-cp313-cp313-win32.whl", hash = "sha256:31cd3a85dbdf1fc002280c65caa7e2b5f65e4a973fcdf70dd2fdcb9868069294", size = 172643 }, - { url = "https://files.pythonhosted.org/packages/cc/4a/fb3c83c1baba64ba90443626c228ca14f19a87c51975d3b1de308dd2cf08/contourpy-1.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:4553c421929ec95fb07b3aaca0fae668b2eb5a5203d1217ca7c34c063c53d087", size = 218301 }, - { url = "https://files.pythonhosted.org/packages/76/65/702f4064f397821fea0cb493f7d3bc95a5d703e20954dce7d6d39bacf378/contourpy-1.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:345af746d7766821d05d72cb8f3845dfd08dd137101a2cb9b24de277d716def8", size = 278972 }, - { url = "https://files.pythonhosted.org/packages/80/85/21f5bba56dba75c10a45ec00ad3b8190dbac7fd9a8a8c46c6116c933e9cf/contourpy-1.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3bb3808858a9dc68f6f03d319acd5f1b8a337e6cdda197f02f4b8ff67ad2057b", size = 263375 }, - { url = "https://files.pythonhosted.org/packages/0a/64/084c86ab71d43149f91ab3a4054ccf18565f0a8af36abfa92b1467813ed6/contourpy-1.3.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:420d39daa61aab1221567b42eecb01112908b2cab7f1b4106a52caaec8d36973", size = 307188 }, - { url = "https://files.pythonhosted.org/packages/3d/ff/d61a4c288dc42da0084b8d9dc2aa219a850767165d7d9a9c364ff530b509/contourpy-1.3.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4d63ee447261e963af02642ffcb864e5a2ee4cbfd78080657a9880b8b1868e18", size = 345644 }, - { url = "https://files.pythonhosted.org/packages/ca/aa/00d2313d35ec03f188e8f0786c2fc61f589306e02fdc158233697546fd58/contourpy-1.3.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:167d6c890815e1dac9536dca00828b445d5d0df4d6a8c6adb4a7ec3166812fa8", size = 317141 }, - { url = "https://files.pythonhosted.org/packages/8d/6a/b5242c8cb32d87f6abf4f5e3044ca397cb1a76712e3fa2424772e3ff495f/contourpy-1.3.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:710a26b3dc80c0e4febf04555de66f5fd17e9cf7170a7b08000601a10570bda6", size = 323469 }, - { url = "https://files.pythonhosted.org/packages/6f/a6/73e929d43028a9079aca4bde107494864d54f0d72d9db508a51ff0878593/contourpy-1.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:75ee7cb1a14c617f34a51d11fa7524173e56551646828353c4af859c56b766e2", size = 1260894 }, - { url = "https://files.pythonhosted.org/packages/2b/1e/1e726ba66eddf21c940821df8cf1a7d15cb165f0682d62161eaa5e93dae1/contourpy-1.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:33c92cdae89ec5135d036e7218e69b0bb2851206077251f04a6c4e0e21f03927", size = 1314829 }, - { url = "https://files.pythonhosted.org/packages/b3/e3/b9f72758adb6ef7397327ceb8b9c39c75711affb220e4f53c745ea1d5a9a/contourpy-1.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a11077e395f67ffc2c44ec2418cfebed032cd6da3022a94fc227b6faf8e2acb8", size = 265518 }, - { url = "https://files.pythonhosted.org/packages/ec/22/19f5b948367ab5260fb41d842c7a78dae645603881ea6bc39738bcfcabf6/contourpy-1.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e8134301d7e204c88ed7ab50028ba06c683000040ede1d617298611f9dc6240c", size = 249350 }, - { url = "https://files.pythonhosted.org/packages/26/76/0c7d43263dd00ae21a91a24381b7e813d286a3294d95d179ef3a7b9fb1d7/contourpy-1.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e12968fdfd5bb45ffdf6192a590bd8ddd3ba9e58360b29683c6bb71a7b41edca", size = 309167 }, - { url = "https://files.pythonhosted.org/packages/96/3b/cadff6773e89f2a5a492c1a8068e21d3fccaf1a1c1df7d65e7c8e3ef60ba/contourpy-1.3.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fd2a0fc506eccaaa7595b7e1418951f213cf8255be2600f1ea1b61e46a60c55f", size = 348279 }, - { url = "https://files.pythonhosted.org/packages/e1/86/158cc43aa549d2081a955ab11c6bdccc7a22caacc2af93186d26f5f48746/contourpy-1.3.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4cfb5c62ce023dfc410d6059c936dcf96442ba40814aefbfa575425a3a7f19dc", size = 318519 }, - { url = "https://files.pythonhosted.org/packages/05/11/57335544a3027e9b96a05948c32e566328e3a2f84b7b99a325b7a06d2b06/contourpy-1.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68a32389b06b82c2fdd68276148d7b9275b5f5cf13e5417e4252f6d1a34f72a2", size = 321922 }, - { url = "https://files.pythonhosted.org/packages/0b/e3/02114f96543f4a1b694333b92a6dcd4f8eebbefcc3a5f3bbb1316634178f/contourpy-1.3.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:94e848a6b83da10898cbf1311a815f770acc9b6a3f2d646f330d57eb4e87592e", size = 1258017 }, - { url = "https://files.pythonhosted.org/packages/f3/3b/bfe4c81c6d5881c1c643dde6620be0b42bf8aab155976dd644595cfab95c/contourpy-1.3.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:d78ab28a03c854a873787a0a42254a0ccb3cb133c672f645c9f9c8f3ae9d0800", size = 1316773 }, - { url = "https://files.pythonhosted.org/packages/f1/17/c52d2970784383cafb0bd918b6fb036d98d96bbf0bc1befb5d1e31a07a70/contourpy-1.3.0-cp39-cp39-win32.whl", hash = "sha256:81cb5ed4952aae6014bc9d0421dec7c5835c9c8c31cdf51910b708f548cf58e5", size = 171353 }, - { url = "https://files.pythonhosted.org/packages/53/23/db9f69676308e094d3c45f20cc52e12d10d64f027541c995d89c11ad5c75/contourpy-1.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:14e262f67bd7e6eb6880bc564dcda30b15e351a594657e55b7eec94b6ef72843", size = 211817 }, - { url = "https://files.pythonhosted.org/packages/d1/09/60e486dc2b64c94ed33e58dcfb6f808192c03dfc5574c016218b9b7680dc/contourpy-1.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:fe41b41505a5a33aeaed2a613dccaeaa74e0e3ead6dd6fd3a118fb471644fd6c", size = 261886 }, - { url = "https://files.pythonhosted.org/packages/19/20/b57f9f7174fcd439a7789fb47d764974ab646fa34d1790551de386457a8e/contourpy-1.3.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eca7e17a65f72a5133bdbec9ecf22401c62bcf4821361ef7811faee695799779", size = 311008 }, - { url = "https://files.pythonhosted.org/packages/74/fc/5040d42623a1845d4f17a418e590fd7a79ae8cb2bad2b2f83de63c3bdca4/contourpy-1.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:1ec4dc6bf570f5b22ed0d7efba0dfa9c5b9e0431aeea7581aa217542d9e809a4", size = 215690 }, - { url = "https://files.pythonhosted.org/packages/2b/24/dc3dcd77ac7460ab7e9d2b01a618cb31406902e50e605a8d6091f0a8f7cc/contourpy-1.3.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:00ccd0dbaad6d804ab259820fa7cb0b8036bda0686ef844d24125d8287178ce0", size = 261894 }, - { url = "https://files.pythonhosted.org/packages/b1/db/531642a01cfec39d1682e46b5457b07cf805e3c3c584ec27e2a6223f8f6c/contourpy-1.3.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ca947601224119117f7c19c9cdf6b3ab54c5726ef1d906aa4a69dfb6dd58102", size = 311099 }, - { url = "https://files.pythonhosted.org/packages/38/1e/94bda024d629f254143a134eead69e21c836429a2a6ce82209a00ddcb79a/contourpy-1.3.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:c6ec93afeb848a0845a18989da3beca3eec2c0f852322efe21af1931147d12cb", size = 215838 }, -] - -[[package]] -name = "contourpy" -version = "1.3.2" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.11'", - "python_full_version == '3.10.*'", -] -dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/66/54/eb9bfc647b19f2009dd5c7f5ec51c4e6ca831725f1aea7a993034f483147/contourpy-1.3.2.tar.gz", hash = "sha256:b6945942715a034c671b7fc54f9588126b0b8bf23db2696e3ca8328f3ff0ab54", size = 13466130 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/12/a3/da4153ec8fe25d263aa48c1a4cbde7f49b59af86f0b6f7862788c60da737/contourpy-1.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ba38e3f9f330af820c4b27ceb4b9c7feee5fe0493ea53a8720f4792667465934", size = 268551 }, - { url = "https://files.pythonhosted.org/packages/2f/6c/330de89ae1087eb622bfca0177d32a7ece50c3ef07b28002de4757d9d875/contourpy-1.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dc41ba0714aa2968d1f8674ec97504a8f7e334f48eeacebcaa6256213acb0989", size = 253399 }, - { url = "https://files.pythonhosted.org/packages/c1/bd/20c6726b1b7f81a8bee5271bed5c165f0a8e1f572578a9d27e2ccb763cb2/contourpy-1.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9be002b31c558d1ddf1b9b415b162c603405414bacd6932d031c5b5a8b757f0d", size = 312061 }, - { url = "https://files.pythonhosted.org/packages/22/fc/a9665c88f8a2473f823cf1ec601de9e5375050f1958cbb356cdf06ef1ab6/contourpy-1.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8d2e74acbcba3bfdb6d9d8384cdc4f9260cae86ed9beee8bd5f54fee49a430b9", size = 351956 }, - { url = "https://files.pythonhosted.org/packages/25/eb/9f0a0238f305ad8fb7ef42481020d6e20cf15e46be99a1fcf939546a177e/contourpy-1.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e259bced5549ac64410162adc973c5e2fb77f04df4a439d00b478e57a0e65512", size = 320872 }, - { url = "https://files.pythonhosted.org/packages/32/5c/1ee32d1c7956923202f00cf8d2a14a62ed7517bdc0ee1e55301227fc273c/contourpy-1.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad687a04bc802cbe8b9c399c07162a3c35e227e2daccf1668eb1f278cb698631", size = 325027 }, - { url = "https://files.pythonhosted.org/packages/83/bf/9baed89785ba743ef329c2b07fd0611d12bfecbedbdd3eeecf929d8d3b52/contourpy-1.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cdd22595308f53ef2f891040ab2b93d79192513ffccbd7fe19be7aa773a5e09f", size = 1306641 }, - { url = "https://files.pythonhosted.org/packages/d4/cc/74e5e83d1e35de2d28bd97033426b450bc4fd96e092a1f7a63dc7369b55d/contourpy-1.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b4f54d6a2defe9f257327b0f243612dd051cc43825587520b1bf74a31e2f6ef2", size = 1374075 }, - { url = "https://files.pythonhosted.org/packages/0c/42/17f3b798fd5e033b46a16f8d9fcb39f1aba051307f5ebf441bad1ecf78f8/contourpy-1.3.2-cp310-cp310-win32.whl", hash = "sha256:f939a054192ddc596e031e50bb13b657ce318cf13d264f095ce9db7dc6ae81c0", size = 177534 }, - { url = "https://files.pythonhosted.org/packages/54/ec/5162b8582f2c994721018d0c9ece9dc6ff769d298a8ac6b6a652c307e7df/contourpy-1.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:c440093bbc8fc21c637c03bafcbef95ccd963bc6e0514ad887932c18ca2a759a", size = 221188 }, - { url = "https://files.pythonhosted.org/packages/b3/b9/ede788a0b56fc5b071639d06c33cb893f68b1178938f3425debebe2dab78/contourpy-1.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6a37a2fb93d4df3fc4c0e363ea4d16f83195fc09c891bc8ce072b9d084853445", size = 269636 }, - { url = "https://files.pythonhosted.org/packages/e6/75/3469f011d64b8bbfa04f709bfc23e1dd71be54d05b1b083be9f5b22750d1/contourpy-1.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b7cd50c38f500bbcc9b6a46643a40e0913673f869315d8e70de0438817cb7773", size = 254636 }, - { url = "https://files.pythonhosted.org/packages/8d/2f/95adb8dae08ce0ebca4fd8e7ad653159565d9739128b2d5977806656fcd2/contourpy-1.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6658ccc7251a4433eebd89ed2672c2ed96fba367fd25ca9512aa92a4b46c4f1", size = 313053 }, - { url = "https://files.pythonhosted.org/packages/c3/a6/8ccf97a50f31adfa36917707fe39c9a0cbc24b3bbb58185577f119736cc9/contourpy-1.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:70771a461aaeb335df14deb6c97439973d253ae70660ca085eec25241137ef43", size = 352985 }, - { url = "https://files.pythonhosted.org/packages/1d/b6/7925ab9b77386143f39d9c3243fdd101621b4532eb126743201160ffa7e6/contourpy-1.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65a887a6e8c4cd0897507d814b14c54a8c2e2aa4ac9f7686292f9769fcf9a6ab", size = 323750 }, - { url = "https://files.pythonhosted.org/packages/c2/f3/20c5d1ef4f4748e52d60771b8560cf00b69d5c6368b5c2e9311bcfa2a08b/contourpy-1.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3859783aefa2b8355697f16642695a5b9792e7a46ab86da1118a4a23a51a33d7", size = 326246 }, - { url = "https://files.pythonhosted.org/packages/8c/e5/9dae809e7e0b2d9d70c52b3d24cba134dd3dad979eb3e5e71f5df22ed1f5/contourpy-1.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:eab0f6db315fa4d70f1d8ab514e527f0366ec021ff853d7ed6a2d33605cf4b83", size = 1308728 }, - { url = "https://files.pythonhosted.org/packages/e2/4a/0058ba34aeea35c0b442ae61a4f4d4ca84d6df8f91309bc2d43bb8dd248f/contourpy-1.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d91a3ccc7fea94ca0acab82ceb77f396d50a1f67412efe4c526f5d20264e6ecd", size = 1375762 }, - { url = "https://files.pythonhosted.org/packages/09/33/7174bdfc8b7767ef2c08ed81244762d93d5c579336fc0b51ca57b33d1b80/contourpy-1.3.2-cp311-cp311-win32.whl", hash = "sha256:1c48188778d4d2f3d48e4643fb15d8608b1d01e4b4d6b0548d9b336c28fc9b6f", size = 178196 }, - { url = "https://files.pythonhosted.org/packages/5e/fe/4029038b4e1c4485cef18e480b0e2cd2d755448bb071eb9977caac80b77b/contourpy-1.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:5ebac872ba09cb8f2131c46b8739a7ff71de28a24c869bcad554477eb089a878", size = 222017 }, - { url = "https://files.pythonhosted.org/packages/34/f7/44785876384eff370c251d58fd65f6ad7f39adce4a093c934d4a67a7c6b6/contourpy-1.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4caf2bcd2969402bf77edc4cb6034c7dd7c0803213b3523f111eb7460a51b8d2", size = 271580 }, - { url = "https://files.pythonhosted.org/packages/93/3b/0004767622a9826ea3d95f0e9d98cd8729015768075d61f9fea8eeca42a8/contourpy-1.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:82199cb78276249796419fe36b7386bd8d2cc3f28b3bc19fe2454fe2e26c4c15", size = 255530 }, - { url = "https://files.pythonhosted.org/packages/e7/bb/7bd49e1f4fa805772d9fd130e0d375554ebc771ed7172f48dfcd4ca61549/contourpy-1.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:106fab697af11456fcba3e352ad50effe493a90f893fca6c2ca5c033820cea92", size = 307688 }, - { url = "https://files.pythonhosted.org/packages/fc/97/e1d5dbbfa170725ef78357a9a0edc996b09ae4af170927ba8ce977e60a5f/contourpy-1.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d14f12932a8d620e307f715857107b1d1845cc44fdb5da2bc8e850f5ceba9f87", size = 347331 }, - { url = "https://files.pythonhosted.org/packages/6f/66/e69e6e904f5ecf6901be3dd16e7e54d41b6ec6ae3405a535286d4418ffb4/contourpy-1.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:532fd26e715560721bb0d5fc7610fce279b3699b018600ab999d1be895b09415", size = 318963 }, - { url = "https://files.pythonhosted.org/packages/a8/32/b8a1c8965e4f72482ff2d1ac2cd670ce0b542f203c8e1d34e7c3e6925da7/contourpy-1.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26b383144cf2d2c29f01a1e8170f50dacf0eac02d64139dcd709a8ac4eb3cfe", size = 323681 }, - { url = "https://files.pythonhosted.org/packages/30/c6/12a7e6811d08757c7162a541ca4c5c6a34c0f4e98ef2b338791093518e40/contourpy-1.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c49f73e61f1f774650a55d221803b101d966ca0c5a2d6d5e4320ec3997489441", size = 1308674 }, - { url = "https://files.pythonhosted.org/packages/2a/8a/bebe5a3f68b484d3a2b8ffaf84704b3e343ef1addea528132ef148e22b3b/contourpy-1.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3d80b2c0300583228ac98d0a927a1ba6a2ba6b8a742463c564f1d419ee5b211e", size = 1380480 }, - { url = "https://files.pythonhosted.org/packages/34/db/fcd325f19b5978fb509a7d55e06d99f5f856294c1991097534360b307cf1/contourpy-1.3.2-cp312-cp312-win32.whl", hash = "sha256:90df94c89a91b7362e1142cbee7568f86514412ab8a2c0d0fca72d7e91b62912", size = 178489 }, - { url = "https://files.pythonhosted.org/packages/01/c8/fadd0b92ffa7b5eb5949bf340a63a4a496a6930a6c37a7ba0f12acb076d6/contourpy-1.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:8c942a01d9163e2e5cfb05cb66110121b8d07ad438a17f9e766317bcb62abf73", size = 223042 }, - { url = "https://files.pythonhosted.org/packages/2e/61/5673f7e364b31e4e7ef6f61a4b5121c5f170f941895912f773d95270f3a2/contourpy-1.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:de39db2604ae755316cb5967728f4bea92685884b1e767b7c24e983ef5f771cb", size = 271630 }, - { url = "https://files.pythonhosted.org/packages/ff/66/a40badddd1223822c95798c55292844b7e871e50f6bfd9f158cb25e0bd39/contourpy-1.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3f9e896f447c5c8618f1edb2bafa9a4030f22a575ec418ad70611450720b5b08", size = 255670 }, - { url = "https://files.pythonhosted.org/packages/1e/c7/cf9fdee8200805c9bc3b148f49cb9482a4e3ea2719e772602a425c9b09f8/contourpy-1.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71e2bd4a1c4188f5c2b8d274da78faab884b59df20df63c34f74aa1813c4427c", size = 306694 }, - { url = "https://files.pythonhosted.org/packages/dd/e7/ccb9bec80e1ba121efbffad7f38021021cda5be87532ec16fd96533bb2e0/contourpy-1.3.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de425af81b6cea33101ae95ece1f696af39446db9682a0b56daaa48cfc29f38f", size = 345986 }, - { url = "https://files.pythonhosted.org/packages/dc/49/ca13bb2da90391fa4219fdb23b078d6065ada886658ac7818e5441448b78/contourpy-1.3.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:977e98a0e0480d3fe292246417239d2d45435904afd6d7332d8455981c408b85", size = 318060 }, - { url = "https://files.pythonhosted.org/packages/c8/65/5245ce8c548a8422236c13ffcdcdada6a2a812c361e9e0c70548bb40b661/contourpy-1.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:434f0adf84911c924519d2b08fc10491dd282b20bdd3fa8f60fd816ea0b48841", size = 322747 }, - { url = "https://files.pythonhosted.org/packages/72/30/669b8eb48e0a01c660ead3752a25b44fdb2e5ebc13a55782f639170772f9/contourpy-1.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c66c4906cdbc50e9cba65978823e6e00b45682eb09adbb78c9775b74eb222422", size = 1308895 }, - { url = "https://files.pythonhosted.org/packages/05/5a/b569f4250decee6e8d54498be7bdf29021a4c256e77fe8138c8319ef8eb3/contourpy-1.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8b7fc0cd78ba2f4695fd0a6ad81a19e7e3ab825c31b577f384aa9d7817dc3bef", size = 1379098 }, - { url = "https://files.pythonhosted.org/packages/19/ba/b227c3886d120e60e41b28740ac3617b2f2b971b9f601c835661194579f1/contourpy-1.3.2-cp313-cp313-win32.whl", hash = "sha256:15ce6ab60957ca74cff444fe66d9045c1fd3e92c8936894ebd1f3eef2fff075f", size = 178535 }, - { url = "https://files.pythonhosted.org/packages/12/6e/2fed56cd47ca739b43e892707ae9a13790a486a3173be063681ca67d2262/contourpy-1.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:e1578f7eafce927b168752ed7e22646dad6cd9bca673c60bff55889fa236ebf9", size = 223096 }, - { url = "https://files.pythonhosted.org/packages/54/4c/e76fe2a03014a7c767d79ea35c86a747e9325537a8b7627e0e5b3ba266b4/contourpy-1.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0475b1f6604896bc7c53bb070e355e9321e1bc0d381735421a2d2068ec56531f", size = 285090 }, - { url = "https://files.pythonhosted.org/packages/7b/e2/5aba47debd55d668e00baf9651b721e7733975dc9fc27264a62b0dd26eb8/contourpy-1.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c85bb486e9be652314bb5b9e2e3b0d1b2e643d5eec4992c0fbe8ac71775da739", size = 268643 }, - { url = "https://files.pythonhosted.org/packages/a1/37/cd45f1f051fe6230f751cc5cdd2728bb3a203f5619510ef11e732109593c/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:745b57db7758f3ffc05a10254edd3182a2a83402a89c00957a8e8a22f5582823", size = 310443 }, - { url = "https://files.pythonhosted.org/packages/8b/a2/36ea6140c306c9ff6dd38e3bcec80b3b018474ef4d17eb68ceecd26675f4/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:970e9173dbd7eba9b4e01aab19215a48ee5dd3f43cef736eebde064a171f89a5", size = 349865 }, - { url = "https://files.pythonhosted.org/packages/95/b7/2fc76bc539693180488f7b6cc518da7acbbb9e3b931fd9280504128bf956/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6c4639a9c22230276b7bffb6a850dfc8258a2521305e1faefe804d006b2e532", size = 321162 }, - { url = "https://files.pythonhosted.org/packages/f4/10/76d4f778458b0aa83f96e59d65ece72a060bacb20cfbee46cf6cd5ceba41/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc829960f34ba36aad4302e78eabf3ef16a3a100863f0d4eeddf30e8a485a03b", size = 327355 }, - { url = "https://files.pythonhosted.org/packages/43/a3/10cf483ea683f9f8ab096c24bad3cce20e0d1dd9a4baa0e2093c1c962d9d/contourpy-1.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d32530b534e986374fc19eaa77fcb87e8a99e5431499949b828312bdcd20ac52", size = 1307935 }, - { url = "https://files.pythonhosted.org/packages/78/73/69dd9a024444489e22d86108e7b913f3528f56cfc312b5c5727a44188471/contourpy-1.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e298e7e70cf4eb179cc1077be1c725b5fd131ebc81181bf0c03525c8abc297fd", size = 1372168 }, - { url = "https://files.pythonhosted.org/packages/0f/1b/96d586ccf1b1a9d2004dd519b25fbf104a11589abfd05484ff12199cca21/contourpy-1.3.2-cp313-cp313t-win32.whl", hash = "sha256:d0e589ae0d55204991450bb5c23f571c64fe43adaa53f93fc902a84c96f52fe1", size = 189550 }, - { url = "https://files.pythonhosted.org/packages/b0/e6/6000d0094e8a5e32ad62591c8609e269febb6e4db83a1c75ff8868b42731/contourpy-1.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:78e9253c3de756b3f6a5174d024c4835acd59eb3f8e2ca13e775dbffe1558f69", size = 238214 }, - { url = "https://files.pythonhosted.org/packages/33/05/b26e3c6ecc05f349ee0013f0bb850a761016d89cec528a98193a48c34033/contourpy-1.3.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:fd93cc7f3139b6dd7aab2f26a90dde0aa9fc264dbf70f6740d498a70b860b82c", size = 265681 }, - { url = "https://files.pythonhosted.org/packages/2b/25/ac07d6ad12affa7d1ffed11b77417d0a6308170f44ff20fa1d5aa6333f03/contourpy-1.3.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:107ba8a6a7eec58bb475329e6d3b95deba9440667c4d62b9b6063942b61d7f16", size = 315101 }, - { url = "https://files.pythonhosted.org/packages/8f/4d/5bb3192bbe9d3f27e3061a6a8e7733c9120e203cb8515767d30973f71030/contourpy-1.3.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ded1706ed0c1049224531b81128efbd5084598f18d8a2d9efae833edbd2b40ad", size = 220599 }, - { url = "https://files.pythonhosted.org/packages/ff/c0/91f1215d0d9f9f343e4773ba6c9b89e8c0cc7a64a6263f21139da639d848/contourpy-1.3.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5f5964cdad279256c084b69c3f412b7801e15356b16efa9d78aa974041903da0", size = 266807 }, - { url = "https://files.pythonhosted.org/packages/d4/79/6be7e90c955c0487e7712660d6cead01fa17bff98e0ea275737cc2bc8e71/contourpy-1.3.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49b65a95d642d4efa8f64ba12558fcb83407e58a2dfba9d796d77b63ccfcaff5", size = 318729 }, - { url = "https://files.pythonhosted.org/packages/87/68/7f46fb537958e87427d98a4074bcde4b67a70b04900cfc5ce29bc2f556c1/contourpy-1.3.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8c5acb8dddb0752bf252e01a3035b21443158910ac16a3b0d20e7fed7d534ce5", size = 221791 }, -] - -[[package]] -name = "cycler" -version = "0.12.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321 }, -] - [[package]] name = "dill" version = "0.4.0" @@ -333,64 +175,13 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/50/3d/9373ad9c56321fdab5b41197068e1d8c25883b3fea29dd361f9b55116869/dill-0.4.0-py3-none-any.whl", hash = "sha256:44f54bf6412c2c8464c14e8243eb163690a9800dbe2c367330883b19c7561049", size = 119668 }, ] -[[package]] -name = "fonttools" -version = "4.59.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8a/27/ec3c723bfdf86f34c5c82bf6305df3e0f0d8ea798d2d3a7cb0c0a866d286/fonttools-4.59.0.tar.gz", hash = "sha256:be392ec3529e2f57faa28709d60723a763904f71a2b63aabe14fee6648fe3b14", size = 3532521 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1c/1f/3dcae710b7c4b56e79442b03db64f6c9f10c3348f7af40339dffcefb581e/fonttools-4.59.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:524133c1be38445c5c0575eacea42dbd44374b310b1ffc4b60ff01d881fabb96", size = 2761846 }, - { url = "https://files.pythonhosted.org/packages/eb/0e/ae3a1884fa1549acac1191cc9ec039142f6ac0e9cbc139c2e6a3dab967da/fonttools-4.59.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:21e606b2d38fed938dde871c5736822dd6bda7a4631b92e509a1f5cd1b90c5df", size = 2332060 }, - { url = "https://files.pythonhosted.org/packages/75/46/58bff92a7216829159ac7bdb1d05a48ad1b8ab8c539555f12d29fdecfdd4/fonttools-4.59.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e93df708c69a193fc7987192f94df250f83f3851fda49413f02ba5dded639482", size = 4852354 }, - { url = "https://files.pythonhosted.org/packages/05/57/767e31e48861045d89691128bd81fd4c62b62150f9a17a666f731ce4f197/fonttools-4.59.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:62224a9bb85b4b66d1b46d45cbe43d71cbf8f527d332b177e3b96191ffbc1e64", size = 4781132 }, - { url = "https://files.pythonhosted.org/packages/d7/78/adb5e9b0af5c6ce469e8b0e112f144eaa84b30dd72a486e9c778a9b03b31/fonttools-4.59.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b8974b2a266b54c96709bd5e239979cddfd2dbceed331aa567ea1d7c4a2202db", size = 4832901 }, - { url = "https://files.pythonhosted.org/packages/ac/92/bc3881097fbf3d56d112bec308c863c058e5d4c9c65f534e8ae58450ab8a/fonttools-4.59.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:209b75943d158f610b78320eacb5539aa9e920bee2c775445b2846c65d20e19d", size = 4940140 }, - { url = "https://files.pythonhosted.org/packages/4a/54/39cdb23f0eeda2e07ae9cb189f2b6f41da89aabc682d3a387b3ff4a4ed29/fonttools-4.59.0-cp310-cp310-win32.whl", hash = "sha256:4c908a7036f0f3677f8afa577bcd973e3e20ddd2f7c42a33208d18bee95cdb6f", size = 2215890 }, - { url = "https://files.pythonhosted.org/packages/d8/eb/f8388d9e19f95d8df2449febe9b1a38ddd758cfdb7d6de3a05198d785d61/fonttools-4.59.0-cp310-cp310-win_amd64.whl", hash = "sha256:8b4309a2775e4feee7356e63b163969a215d663399cce1b3d3b65e7ec2d9680e", size = 2260191 }, - { url = "https://files.pythonhosted.org/packages/06/96/520733d9602fa1bf6592e5354c6721ac6fc9ea72bc98d112d0c38b967199/fonttools-4.59.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:841b2186adce48903c0fef235421ae21549020eca942c1da773ac380b056ab3c", size = 2782387 }, - { url = "https://files.pythonhosted.org/packages/87/6a/170fce30b9bce69077d8eec9bea2cfd9f7995e8911c71be905e2eba6368b/fonttools-4.59.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9bcc1e77fbd1609198966ded6b2a9897bd6c6bcbd2287a2fc7d75f1a254179c5", size = 2342194 }, - { url = "https://files.pythonhosted.org/packages/b0/b6/7c8166c0066856f1408092f7968ac744060cf72ca53aec9036106f57eeca/fonttools-4.59.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:37c377f7cb2ab2eca8a0b319c68146d34a339792f9420fca6cd49cf28d370705", size = 5032333 }, - { url = "https://files.pythonhosted.org/packages/eb/0c/707c5a19598eafcafd489b73c4cb1c142102d6197e872f531512d084aa76/fonttools-4.59.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fa39475eaccb98f9199eccfda4298abaf35ae0caec676ffc25b3a5e224044464", size = 4974422 }, - { url = "https://files.pythonhosted.org/packages/f6/e7/6d33737d9fe632a0f59289b6f9743a86d2a9d0673de2a0c38c0f54729822/fonttools-4.59.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d3972b13148c1d1fbc092b27678a33b3080d1ac0ca305742b0119b75f9e87e38", size = 5010631 }, - { url = "https://files.pythonhosted.org/packages/63/e1/a4c3d089ab034a578820c8f2dff21ef60daf9668034a1e4fb38bb1cc3398/fonttools-4.59.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a408c3c51358c89b29cfa5317cf11518b7ce5de1717abb55c5ae2d2921027de6", size = 5122198 }, - { url = "https://files.pythonhosted.org/packages/09/77/ca82b9c12fa4de3c520b7760ee61787640cf3fde55ef1b0bfe1de38c8153/fonttools-4.59.0-cp311-cp311-win32.whl", hash = "sha256:6770d7da00f358183d8fd5c4615436189e4f683bdb6affb02cad3d221d7bb757", size = 2214216 }, - { url = "https://files.pythonhosted.org/packages/ab/25/5aa7ca24b560b2f00f260acf32c4cf29d7aaf8656e159a336111c18bc345/fonttools-4.59.0-cp311-cp311-win_amd64.whl", hash = "sha256:84fc186980231a287b28560d3123bd255d3c6b6659828c642b4cf961e2b923d0", size = 2261879 }, - { url = "https://files.pythonhosted.org/packages/e2/77/b1c8af22f4265e951cd2e5535dbef8859efcef4fb8dee742d368c967cddb/fonttools-4.59.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f9b3a78f69dcbd803cf2fb3f972779875b244c1115481dfbdd567b2c22b31f6b", size = 2767562 }, - { url = "https://files.pythonhosted.org/packages/ff/5a/aeb975699588176bb357e8b398dfd27e5d3a2230d92b81ab8cbb6187358d/fonttools-4.59.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:57bb7e26928573ee7c6504f54c05860d867fd35e675769f3ce01b52af38d48e2", size = 2335168 }, - { url = "https://files.pythonhosted.org/packages/54/97/c6101a7e60ae138c4ef75b22434373a0da50a707dad523dd19a4889315bf/fonttools-4.59.0-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4536f2695fe5c1ffb528d84a35a7d3967e5558d2af58b4775e7ab1449d65767b", size = 4909850 }, - { url = "https://files.pythonhosted.org/packages/bd/6c/fa4d18d641054f7bff878cbea14aa9433f292b9057cb1700d8e91a4d5f4f/fonttools-4.59.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:885bde7d26e5b40e15c47bd5def48b38cbd50830a65f98122a8fb90962af7cd1", size = 4955131 }, - { url = "https://files.pythonhosted.org/packages/20/5c/331947fc1377deb928a69bde49f9003364f5115e5cbe351eea99e39412a2/fonttools-4.59.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6801aeddb6acb2c42eafa45bc1cb98ba236871ae6f33f31e984670b749a8e58e", size = 4899667 }, - { url = "https://files.pythonhosted.org/packages/8a/46/b66469dfa26b8ff0baa7654b2cc7851206c6d57fe3abdabbaab22079a119/fonttools-4.59.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:31003b6a10f70742a63126b80863ab48175fb8272a18ca0846c0482968f0588e", size = 5051349 }, - { url = "https://files.pythonhosted.org/packages/2e/05/ebfb6b1f3a4328ab69787d106a7d92ccde77ce66e98659df0f9e3f28d93d/fonttools-4.59.0-cp312-cp312-win32.whl", hash = "sha256:fbce6dae41b692a5973d0f2158f782b9ad05babc2c2019a970a1094a23909b1b", size = 2201315 }, - { url = "https://files.pythonhosted.org/packages/09/45/d2bdc9ea20bbadec1016fd0db45696d573d7a26d95ab5174ffcb6d74340b/fonttools-4.59.0-cp312-cp312-win_amd64.whl", hash = "sha256:332bfe685d1ac58ca8d62b8d6c71c2e52a6c64bc218dc8f7825c9ea51385aa01", size = 2249408 }, - { url = "https://files.pythonhosted.org/packages/f3/bb/390990e7c457d377b00890d9f96a3ca13ae2517efafb6609c1756e213ba4/fonttools-4.59.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:78813b49d749e1bb4db1c57f2d4d7e6db22c253cb0a86ad819f5dc197710d4b2", size = 2758704 }, - { url = "https://files.pythonhosted.org/packages/df/6f/d730d9fcc9b410a11597092bd2eb9ca53e5438c6cb90e4b3047ce1b723e9/fonttools-4.59.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:401b1941ce37e78b8fd119b419b617277c65ae9417742a63282257434fd68ea2", size = 2330764 }, - { url = "https://files.pythonhosted.org/packages/75/b4/b96bb66f6f8cc4669de44a158099b249c8159231d254ab6b092909388be5/fonttools-4.59.0-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:efd7e6660674e234e29937bc1481dceb7e0336bfae75b856b4fb272b5093c5d4", size = 4890699 }, - { url = "https://files.pythonhosted.org/packages/b5/57/7969af50b26408be12baa317c6147588db5b38af2759e6df94554dbc5fdb/fonttools-4.59.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51ab1ff33c19e336c02dee1e9fd1abd974a4ca3d8f7eef2a104d0816a241ce97", size = 4952934 }, - { url = "https://files.pythonhosted.org/packages/d6/e2/dd968053b6cf1f46c904f5bd409b22341477c017d8201619a265e50762d3/fonttools-4.59.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a9bf8adc9e1f3012edc8f09b08336272aec0c55bc677422273e21280db748f7c", size = 4892319 }, - { url = "https://files.pythonhosted.org/packages/6b/95/a59810d8eda09129f83467a4e58f84205dc6994ebaeb9815406363e07250/fonttools-4.59.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:37e01c6ec0c98599778c2e688350d624fa4770fbd6144551bd5e032f1199171c", size = 5034753 }, - { url = "https://files.pythonhosted.org/packages/a5/84/51a69ee89ff8d1fea0c6997e946657e25a3f08513de8435fe124929f3eef/fonttools-4.59.0-cp313-cp313-win32.whl", hash = "sha256:70d6b3ceaa9cc5a6ac52884f3b3d9544e8e231e95b23f138bdb78e6d4dc0eae3", size = 2199688 }, - { url = "https://files.pythonhosted.org/packages/a0/ee/f626cd372932d828508137a79b85167fdcf3adab2e3bed433f295c596c6a/fonttools-4.59.0-cp313-cp313-win_amd64.whl", hash = "sha256:26731739daa23b872643f0e4072d5939960237d540c35c14e6a06d47d71ca8fe", size = 2248560 }, - { url = "https://files.pythonhosted.org/packages/c5/68/635adfcd75d86a965f633ea704308a762ee7e80f000456da010eadd3b032/fonttools-4.59.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8d77f92438daeaddc05682f0f3dac90c5b9829bcac75b57e8ce09cb67786073c", size = 2768038 }, - { url = "https://files.pythonhosted.org/packages/d4/c7/41812171da0337a4d3e58da0fe9e13df55990a8e48d1babf1ece2f48a717/fonttools-4.59.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:60f6665579e909b618282f3c14fa0b80570fbf1ee0e67678b9a9d43aa5d67a37", size = 2335207 }, - { url = "https://files.pythonhosted.org/packages/c9/40/0b1c47982ccb8c5eec15ddae486ccdf34364c2683307e139f877c6a4710f/fonttools-4.59.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:169b99a2553a227f7b5fea8d9ecd673aa258617f466b2abc6091fe4512a0dcd0", size = 4832505 }, - { url = "https://files.pythonhosted.org/packages/ee/40/70cfe1b4a3f6218457e76ce0743e692cb82a4e5c8a9a1fe64576428488a2/fonttools-4.59.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:052444a5d0151878e87e3e512a1aa1a0ab35ee4c28afde0a778e23b0ace4a7de", size = 4762567 }, - { url = "https://files.pythonhosted.org/packages/d4/18/5231342b4e528eb8d2c048f4663cf7dc892dee51387f5a0383b8e9e49283/fonttools-4.59.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:d40dcf533ca481355aa7b682e9e079f766f35715defa4929aeb5597f9604272e", size = 4815520 }, - { url = "https://files.pythonhosted.org/packages/91/b3/7661184576e235f84ed6ff232d287c598fb517224c5dfad8ae67fdd158e5/fonttools-4.59.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b818db35879d2edf7f46c7e729c700a0bce03b61b9412f5a7118406687cb151d", size = 4924601 }, - { url = "https://files.pythonhosted.org/packages/cf/96/dfd52d0e603c2c03f1b6153a1989c810a2083f8ad282b8b80acf3fe736f8/fonttools-4.59.0-cp39-cp39-win32.whl", hash = "sha256:2e7cf8044ce2598bb87e44ba1d2c6e45d7a8decf56055b92906dc53f67c76d64", size = 1485238 }, - { url = "https://files.pythonhosted.org/packages/3b/75/efc6486371cc1125e41fc0c149d80605e17ce7fc28c05cc33d503b0bf41f/fonttools-4.59.0-cp39-cp39-win_amd64.whl", hash = "sha256:902425f5afe28572d65d2bf9c33edd5265c612ff82c69e6f83ea13eafc0dcbea", size = 1530070 }, - { url = "https://files.pythonhosted.org/packages/d0/9c/df0ef2c51845a13043e5088f7bb988ca6cd5bb82d5d4203d6a158aa58cf2/fonttools-4.59.0-py3-none-any.whl", hash = "sha256:241313683afd3baacb32a6bd124d0bce7404bc5280e12e291bae1b9bba28711d", size = 1128050 }, -] - [[package]] name = "greyjack" -version = "0.3.3" +version = "0.3.6" source = { editable = "." } dependencies = [ { name = "bitarray" }, { name = "dill" }, - { name = "matplotlib", version = "3.9.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "matplotlib", version = "3.10.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "maturin" }, { name = "mmh3" }, { name = "multiprocess" }, @@ -411,7 +202,6 @@ dependencies = [ requires-dist = [ { name = "bitarray", specifier = ">=3.5.0" }, { name = "dill" }, - { name = "matplotlib", specifier = ">=3.9.4" }, { name = "maturin", specifier = ">=1.9.1" }, { name = "mmh3", specifier = ">=5.1.0" }, { name = "multiprocess" }, @@ -426,212 +216,6 @@ requires-dist = [ { name = "pyzmq" }, ] -[[package]] -name = "importlib-resources" -version = "6.5.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "zipp", marker = "python_full_version < '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/cf/8c/f834fbf984f691b4f7ff60f50b514cc3de5cc08abfc3295564dd89c5e2e7/importlib_resources-6.5.2.tar.gz", hash = "sha256:185f87adef5bcc288449d98fb4fba07cea78bc036455dd44c5fc4a2fe78fed2c", size = 44693 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a4/ed/1f1afb2e9e7f38a545d628f864d562a5ae64fe6f7a10e28ffb9b185b4e89/importlib_resources-6.5.2-py3-none-any.whl", hash = "sha256:789cfdc3ed28c78b67a06acb8126751ced69a3d5f79c095a98298cd8a760ccec", size = 37461 }, -] - -[[package]] -name = "kiwisolver" -version = "1.4.7" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10'", -] -sdist = { url = "https://files.pythonhosted.org/packages/85/4d/2255e1c76304cbd60b48cee302b66d1dde4468dc5b1160e4b7cb43778f2a/kiwisolver-1.4.7.tar.gz", hash = "sha256:9893ff81bd7107f7b685d3017cc6583daadb4fc26e4a888350df530e41980a60", size = 97286 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/97/14/fc943dd65268a96347472b4fbe5dcc2f6f55034516f80576cd0dd3a8930f/kiwisolver-1.4.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8a9c83f75223d5e48b0bc9cb1bf2776cf01563e00ade8775ffe13b0b6e1af3a6", size = 122440 }, - { url = "https://files.pythonhosted.org/packages/1e/46/e68fed66236b69dd02fcdb506218c05ac0e39745d696d22709498896875d/kiwisolver-1.4.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:58370b1ffbd35407444d57057b57da5d6549d2d854fa30249771775c63b5fe17", size = 65758 }, - { url = "https://files.pythonhosted.org/packages/ef/fa/65de49c85838681fc9cb05de2a68067a683717321e01ddafb5b8024286f0/kiwisolver-1.4.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:aa0abdf853e09aff551db11fce173e2177d00786c688203f52c87ad7fcd91ef9", size = 64311 }, - { url = "https://files.pythonhosted.org/packages/42/9c/cc8d90f6ef550f65443bad5872ffa68f3dee36de4974768628bea7c14979/kiwisolver-1.4.7-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:8d53103597a252fb3ab8b5845af04c7a26d5e7ea8122303dd7a021176a87e8b9", size = 1637109 }, - { url = "https://files.pythonhosted.org/packages/55/91/0a57ce324caf2ff5403edab71c508dd8f648094b18cfbb4c8cc0fde4a6ac/kiwisolver-1.4.7-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:88f17c5ffa8e9462fb79f62746428dd57b46eb931698e42e990ad63103f35e6c", size = 1617814 }, - { url = "https://files.pythonhosted.org/packages/12/5d/c36140313f2510e20207708adf36ae4919416d697ee0236b0ddfb6fd1050/kiwisolver-1.4.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88a9ca9c710d598fd75ee5de59d5bda2684d9db36a9f50b6125eaea3969c2599", size = 1400881 }, - { url = "https://files.pythonhosted.org/packages/56/d0/786e524f9ed648324a466ca8df86298780ef2b29c25313d9a4f16992d3cf/kiwisolver-1.4.7-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f4d742cb7af1c28303a51b7a27aaee540e71bb8e24f68c736f6f2ffc82f2bf05", size = 1512972 }, - { url = "https://files.pythonhosted.org/packages/67/5a/77851f2f201e6141d63c10a0708e996a1363efaf9e1609ad0441b343763b/kiwisolver-1.4.7-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e28c7fea2196bf4c2f8d46a0415c77a1c480cc0724722f23d7410ffe9842c407", size = 1444787 }, - { url = "https://files.pythonhosted.org/packages/06/5f/1f5eaab84355885e224a6fc8d73089e8713dc7e91c121f00b9a1c58a2195/kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e968b84db54f9d42046cf154e02911e39c0435c9801681e3fc9ce8a3c4130278", size = 2199212 }, - { url = "https://files.pythonhosted.org/packages/b5/28/9152a3bfe976a0ae21d445415defc9d1cd8614b2910b7614b30b27a47270/kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0c18ec74c0472de033e1bebb2911c3c310eef5649133dd0bedf2a169a1b269e5", size = 2346399 }, - { url = "https://files.pythonhosted.org/packages/26/f6/453d1904c52ac3b400f4d5e240ac5fec25263716723e44be65f4d7149d13/kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8f0ea6da6d393d8b2e187e6a5e3fb81f5862010a40c3945e2c6d12ae45cfb2ad", size = 2308688 }, - { url = "https://files.pythonhosted.org/packages/5a/9a/d4968499441b9ae187e81745e3277a8b4d7c60840a52dc9d535a7909fac3/kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:f106407dda69ae456dd1227966bf445b157ccc80ba0dff3802bb63f30b74e895", size = 2445493 }, - { url = "https://files.pythonhosted.org/packages/07/c9/032267192e7828520dacb64dfdb1d74f292765f179e467c1cba97687f17d/kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:84ec80df401cfee1457063732d90022f93951944b5b58975d34ab56bb150dfb3", size = 2262191 }, - { url = "https://files.pythonhosted.org/packages/6c/ad/db0aedb638a58b2951da46ddaeecf204be8b4f5454df020d850c7fa8dca8/kiwisolver-1.4.7-cp310-cp310-win32.whl", hash = "sha256:71bb308552200fb2c195e35ef05de12f0c878c07fc91c270eb3d6e41698c3bcc", size = 46644 }, - { url = "https://files.pythonhosted.org/packages/12/ca/d0f7b7ffbb0be1e7c2258b53554efec1fd652921f10d7d85045aff93ab61/kiwisolver-1.4.7-cp310-cp310-win_amd64.whl", hash = "sha256:44756f9fd339de0fb6ee4f8c1696cfd19b2422e0d70b4cefc1cc7f1f64045a8c", size = 55877 }, - { url = "https://files.pythonhosted.org/packages/97/6c/cfcc128672f47a3e3c0d918ecb67830600078b025bfc32d858f2e2d5c6a4/kiwisolver-1.4.7-cp310-cp310-win_arm64.whl", hash = "sha256:78a42513018c41c2ffd262eb676442315cbfe3c44eed82385c2ed043bc63210a", size = 48347 }, - { url = "https://files.pythonhosted.org/packages/e9/44/77429fa0a58f941d6e1c58da9efe08597d2e86bf2b2cce6626834f49d07b/kiwisolver-1.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d2b0e12a42fb4e72d509fc994713d099cbb15ebf1103545e8a45f14da2dfca54", size = 122442 }, - { url = "https://files.pythonhosted.org/packages/e5/20/8c75caed8f2462d63c7fd65e16c832b8f76cda331ac9e615e914ee80bac9/kiwisolver-1.4.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2a8781ac3edc42ea4b90bc23e7d37b665d89423818e26eb6df90698aa2287c95", size = 65762 }, - { url = "https://files.pythonhosted.org/packages/f4/98/fe010f15dc7230f45bc4cf367b012d651367fd203caaa992fd1f5963560e/kiwisolver-1.4.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:46707a10836894b559e04b0fd143e343945c97fd170d69a2d26d640b4e297935", size = 64319 }, - { url = "https://files.pythonhosted.org/packages/8b/1b/b5d618f4e58c0675654c1e5051bcf42c776703edb21c02b8c74135541f60/kiwisolver-1.4.7-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef97b8df011141c9b0f6caf23b29379f87dd13183c978a30a3c546d2c47314cb", size = 1334260 }, - { url = "https://files.pythonhosted.org/packages/b8/01/946852b13057a162a8c32c4c8d2e9ed79f0bb5d86569a40c0b5fb103e373/kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ab58c12a2cd0fc769089e6d38466c46d7f76aced0a1f54c77652446733d2d02", size = 1426589 }, - { url = "https://files.pythonhosted.org/packages/70/d1/c9f96df26b459e15cf8a965304e6e6f4eb291e0f7a9460b4ad97b047561e/kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:803b8e1459341c1bb56d1c5c010406d5edec8a0713a0945851290a7930679b51", size = 1541080 }, - { url = "https://files.pythonhosted.org/packages/d3/73/2686990eb8b02d05f3de759d6a23a4ee7d491e659007dd4c075fede4b5d0/kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f9a9e8a507420fe35992ee9ecb302dab68550dedc0da9e2880dd88071c5fb052", size = 1470049 }, - { url = "https://files.pythonhosted.org/packages/a7/4b/2db7af3ed3af7c35f388d5f53c28e155cd402a55432d800c543dc6deb731/kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18077b53dc3bb490e330669a99920c5e6a496889ae8c63b58fbc57c3d7f33a18", size = 1426376 }, - { url = "https://files.pythonhosted.org/packages/05/83/2857317d04ea46dc5d115f0df7e676997bbd968ced8e2bd6f7f19cfc8d7f/kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6af936f79086a89b3680a280c47ea90b4df7047b5bdf3aa5c524bbedddb9e545", size = 2222231 }, - { url = "https://files.pythonhosted.org/packages/0d/b5/866f86f5897cd4ab6d25d22e403404766a123f138bd6a02ecb2cdde52c18/kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:3abc5b19d24af4b77d1598a585b8a719beb8569a71568b66f4ebe1fb0449460b", size = 2368634 }, - { url = "https://files.pythonhosted.org/packages/c1/ee/73de8385403faba55f782a41260210528fe3273d0cddcf6d51648202d6d0/kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:933d4de052939d90afbe6e9d5273ae05fb836cc86c15b686edd4b3560cc0ee36", size = 2329024 }, - { url = "https://files.pythonhosted.org/packages/a1/e7/cd101d8cd2cdfaa42dc06c433df17c8303d31129c9fdd16c0ea37672af91/kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:65e720d2ab2b53f1f72fb5da5fb477455905ce2c88aaa671ff0a447c2c80e8e3", size = 2468484 }, - { url = "https://files.pythonhosted.org/packages/e1/72/84f09d45a10bc57a40bb58b81b99d8f22b58b2040c912b7eb97ebf625bf2/kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3bf1ed55088f214ba6427484c59553123fdd9b218a42bbc8c6496d6754b1e523", size = 2284078 }, - { url = "https://files.pythonhosted.org/packages/d2/d4/71828f32b956612dc36efd7be1788980cb1e66bfb3706e6dec9acad9b4f9/kiwisolver-1.4.7-cp311-cp311-win32.whl", hash = "sha256:4c00336b9dd5ad96d0a558fd18a8b6f711b7449acce4c157e7343ba92dd0cf3d", size = 46645 }, - { url = "https://files.pythonhosted.org/packages/a1/65/d43e9a20aabcf2e798ad1aff6c143ae3a42cf506754bcb6a7ed8259c8425/kiwisolver-1.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:929e294c1ac1e9f615c62a4e4313ca1823ba37326c164ec720a803287c4c499b", size = 56022 }, - { url = "https://files.pythonhosted.org/packages/35/b3/9f75a2e06f1b4ca00b2b192bc2b739334127d27f1d0625627ff8479302ba/kiwisolver-1.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:e33e8fbd440c917106b237ef1a2f1449dfbb9b6f6e1ce17c94cd6a1e0d438376", size = 48536 }, - { url = "https://files.pythonhosted.org/packages/97/9c/0a11c714cf8b6ef91001c8212c4ef207f772dd84540104952c45c1f0a249/kiwisolver-1.4.7-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:5360cc32706dab3931f738d3079652d20982511f7c0ac5711483e6eab08efff2", size = 121808 }, - { url = "https://files.pythonhosted.org/packages/f2/d8/0fe8c5f5d35878ddd135f44f2af0e4e1d379e1c7b0716f97cdcb88d4fd27/kiwisolver-1.4.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:942216596dc64ddb25adb215c3c783215b23626f8d84e8eff8d6d45c3f29f75a", size = 65531 }, - { url = "https://files.pythonhosted.org/packages/80/c5/57fa58276dfdfa612241d640a64ca2f76adc6ffcebdbd135b4ef60095098/kiwisolver-1.4.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:48b571ecd8bae15702e4f22d3ff6a0f13e54d3d00cd25216d5e7f658242065ee", size = 63894 }, - { url = "https://files.pythonhosted.org/packages/8b/e9/26d3edd4c4ad1c5b891d8747a4f81b1b0aba9fb9721de6600a4adc09773b/kiwisolver-1.4.7-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ad42ba922c67c5f219097b28fae965e10045ddf145d2928bfac2eb2e17673640", size = 1369296 }, - { url = "https://files.pythonhosted.org/packages/b6/67/3f4850b5e6cffb75ec40577ddf54f7b82b15269cc5097ff2e968ee32ea7d/kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:612a10bdae23404a72941a0fc8fa2660c6ea1217c4ce0dbcab8a8f6543ea9e7f", size = 1461450 }, - { url = "https://files.pythonhosted.org/packages/52/be/86cbb9c9a315e98a8dc6b1d23c43cffd91d97d49318854f9c37b0e41cd68/kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9e838bba3a3bac0fe06d849d29772eb1afb9745a59710762e4ba3f4cb8424483", size = 1579168 }, - { url = "https://files.pythonhosted.org/packages/0f/00/65061acf64bd5fd34c1f4ae53f20b43b0a017a541f242a60b135b9d1e301/kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:22f499f6157236c19f4bbbd472fa55b063db77a16cd74d49afe28992dff8c258", size = 1507308 }, - { url = "https://files.pythonhosted.org/packages/21/e4/c0b6746fd2eb62fe702118b3ca0cb384ce95e1261cfada58ff693aeec08a/kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:693902d433cf585133699972b6d7c42a8b9f8f826ebcaf0132ff55200afc599e", size = 1464186 }, - { url = "https://files.pythonhosted.org/packages/0a/0f/529d0a9fffb4d514f2782c829b0b4b371f7f441d61aa55f1de1c614c4ef3/kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4e77f2126c3e0b0d055f44513ed349038ac180371ed9b52fe96a32aa071a5107", size = 2247877 }, - { url = "https://files.pythonhosted.org/packages/d1/e1/66603ad779258843036d45adcbe1af0d1a889a07af4635f8b4ec7dccda35/kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:657a05857bda581c3656bfc3b20e353c232e9193eb167766ad2dc58b56504948", size = 2404204 }, - { url = "https://files.pythonhosted.org/packages/8d/61/de5fb1ca7ad1f9ab7970e340a5b833d735df24689047de6ae71ab9d8d0e7/kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4bfa75a048c056a411f9705856abfc872558e33c055d80af6a380e3658766038", size = 2352461 }, - { url = "https://files.pythonhosted.org/packages/ba/d2/0edc00a852e369827f7e05fd008275f550353f1f9bcd55db9363d779fc63/kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:34ea1de54beef1c104422d210c47c7d2a4999bdecf42c7b5718fbe59a4cac383", size = 2501358 }, - { url = "https://files.pythonhosted.org/packages/84/15/adc15a483506aec6986c01fb7f237c3aec4d9ed4ac10b756e98a76835933/kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:90da3b5f694b85231cf93586dad5e90e2d71b9428f9aad96952c99055582f520", size = 2314119 }, - { url = "https://files.pythonhosted.org/packages/36/08/3a5bb2c53c89660863a5aa1ee236912269f2af8762af04a2e11df851d7b2/kiwisolver-1.4.7-cp312-cp312-win32.whl", hash = "sha256:18e0cca3e008e17fe9b164b55735a325140a5a35faad8de92dd80265cd5eb80b", size = 46367 }, - { url = "https://files.pythonhosted.org/packages/19/93/c05f0a6d825c643779fc3c70876bff1ac221f0e31e6f701f0e9578690d70/kiwisolver-1.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:58cb20602b18f86f83a5c87d3ee1c766a79c0d452f8def86d925e6c60fbf7bfb", size = 55884 }, - { url = "https://files.pythonhosted.org/packages/d2/f9/3828d8f21b6de4279f0667fb50a9f5215e6fe57d5ec0d61905914f5b6099/kiwisolver-1.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:f5a8b53bdc0b3961f8b6125e198617c40aeed638b387913bf1ce78afb1b0be2a", size = 48528 }, - { url = "https://files.pythonhosted.org/packages/c4/06/7da99b04259b0f18b557a4effd1b9c901a747f7fdd84cf834ccf520cb0b2/kiwisolver-1.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2e6039dcbe79a8e0f044f1c39db1986a1b8071051efba3ee4d74f5b365f5226e", size = 121913 }, - { url = "https://files.pythonhosted.org/packages/97/f5/b8a370d1aa593c17882af0a6f6755aaecd643640c0ed72dcfd2eafc388b9/kiwisolver-1.4.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a1ecf0ac1c518487d9d23b1cd7139a6a65bc460cd101ab01f1be82ecf09794b6", size = 65627 }, - { url = "https://files.pythonhosted.org/packages/2a/fc/6c0374f7503522539e2d4d1b497f5ebad3f8ed07ab51aed2af988dd0fb65/kiwisolver-1.4.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7ab9ccab2b5bd5702ab0803676a580fffa2aa178c2badc5557a84cc943fcf750", size = 63888 }, - { url = "https://files.pythonhosted.org/packages/bf/3e/0b7172793d0f41cae5c923492da89a2ffcd1adf764c16159ca047463ebd3/kiwisolver-1.4.7-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f816dd2277f8d63d79f9c8473a79fe54047bc0467754962840782c575522224d", size = 1369145 }, - { url = "https://files.pythonhosted.org/packages/77/92/47d050d6f6aced2d634258123f2688fbfef8ded3c5baf2c79d94d91f1f58/kiwisolver-1.4.7-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf8bcc23ceb5a1b624572a1623b9f79d2c3b337c8c455405ef231933a10da379", size = 1461448 }, - { url = "https://files.pythonhosted.org/packages/9c/1b/8f80b18e20b3b294546a1adb41701e79ae21915f4175f311a90d042301cf/kiwisolver-1.4.7-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dea0bf229319828467d7fca8c7c189780aa9ff679c94539eed7532ebe33ed37c", size = 1578750 }, - { url = "https://files.pythonhosted.org/packages/a4/fe/fe8e72f3be0a844f257cadd72689c0848c6d5c51bc1d60429e2d14ad776e/kiwisolver-1.4.7-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c06a4c7cf15ec739ce0e5971b26c93638730090add60e183530d70848ebdd34", size = 1507175 }, - { url = "https://files.pythonhosted.org/packages/39/fa/cdc0b6105d90eadc3bee525fecc9179e2b41e1ce0293caaf49cb631a6aaf/kiwisolver-1.4.7-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:913983ad2deb14e66d83c28b632fd35ba2b825031f2fa4ca29675e665dfecbe1", size = 1463963 }, - { url = "https://files.pythonhosted.org/packages/6e/5c/0c03c4e542720c6177d4f408e56d1c8315899db72d46261a4e15b8b33a41/kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5337ec7809bcd0f424c6b705ecf97941c46279cf5ed92311782c7c9c2026f07f", size = 2248220 }, - { url = "https://files.pythonhosted.org/packages/3d/ee/55ef86d5a574f4e767df7da3a3a7ff4954c996e12d4fbe9c408170cd7dcc/kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4c26ed10c4f6fa6ddb329a5120ba3b6db349ca192ae211e882970bfc9d91420b", size = 2404463 }, - { url = "https://files.pythonhosted.org/packages/0f/6d/73ad36170b4bff4825dc588acf4f3e6319cb97cd1fb3eb04d9faa6b6f212/kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c619b101e6de2222c1fcb0531e1b17bbffbe54294bfba43ea0d411d428618c27", size = 2352842 }, - { url = "https://files.pythonhosted.org/packages/0b/16/fa531ff9199d3b6473bb4d0f47416cdb08d556c03b8bc1cccf04e756b56d/kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:073a36c8273647592ea332e816e75ef8da5c303236ec0167196793eb1e34657a", size = 2501635 }, - { url = "https://files.pythonhosted.org/packages/78/7e/aa9422e78419db0cbe75fb86d8e72b433818f2e62e2e394992d23d23a583/kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3ce6b2b0231bda412463e152fc18335ba32faf4e8c23a754ad50ffa70e4091ee", size = 2314556 }, - { url = "https://files.pythonhosted.org/packages/a8/b2/15f7f556df0a6e5b3772a1e076a9d9f6c538ce5f05bd590eca8106508e06/kiwisolver-1.4.7-cp313-cp313-win32.whl", hash = "sha256:f4c9aee212bc89d4e13f58be11a56cc8036cabad119259d12ace14b34476fd07", size = 46364 }, - { url = "https://files.pythonhosted.org/packages/0b/db/32e897e43a330eee8e4770bfd2737a9584b23e33587a0812b8e20aac38f7/kiwisolver-1.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:8a3ec5aa8e38fc4c8af308917ce12c536f1c88452ce554027e55b22cbbfbff76", size = 55887 }, - { url = "https://files.pythonhosted.org/packages/c8/a4/df2bdca5270ca85fd25253049eb6708d4127be2ed0e5c2650217450b59e9/kiwisolver-1.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:76c8094ac20ec259471ac53e774623eb62e6e1f56cd8690c67ce6ce4fcb05650", size = 48530 }, - { url = "https://files.pythonhosted.org/packages/11/88/37ea0ea64512997b13d69772db8dcdc3bfca5442cda3a5e4bb943652ee3e/kiwisolver-1.4.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3f9362ecfca44c863569d3d3c033dbe8ba452ff8eed6f6b5806382741a1334bd", size = 122449 }, - { url = "https://files.pythonhosted.org/packages/4e/45/5a5c46078362cb3882dcacad687c503089263c017ca1241e0483857791eb/kiwisolver-1.4.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e8df2eb9b2bac43ef8b082e06f750350fbbaf2887534a5be97f6cf07b19d9583", size = 65757 }, - { url = "https://files.pythonhosted.org/packages/8a/be/a6ae58978772f685d48dd2e84460937761c53c4bbd84e42b0336473d9775/kiwisolver-1.4.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f32d6edbc638cde7652bd690c3e728b25332acbadd7cad670cc4a02558d9c417", size = 64312 }, - { url = "https://files.pythonhosted.org/packages/f4/04/18ef6f452d311e1e1eb180c9bf5589187fa1f042db877e6fe443ef10099c/kiwisolver-1.4.7-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e2e6c39bd7b9372b0be21456caab138e8e69cc0fc1190a9dfa92bd45a1e6e904", size = 1626966 }, - { url = "https://files.pythonhosted.org/packages/21/b1/40655f6c3fa11ce740e8a964fa8e4c0479c87d6a7944b95af799c7a55dfe/kiwisolver-1.4.7-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:dda56c24d869b1193fcc763f1284b9126550eaf84b88bbc7256e15028f19188a", size = 1607044 }, - { url = "https://files.pythonhosted.org/packages/fd/93/af67dbcfb9b3323bbd2c2db1385a7139d8f77630e4a37bb945b57188eb2d/kiwisolver-1.4.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79849239c39b5e1fd906556c474d9b0439ea6792b637511f3fe3a41158d89ca8", size = 1391879 }, - { url = "https://files.pythonhosted.org/packages/40/6f/d60770ef98e77b365d96061d090c0cd9e23418121c55fff188fa4bdf0b54/kiwisolver-1.4.7-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5e3bc157fed2a4c02ec468de4ecd12a6e22818d4f09cde2c31ee3226ffbefab2", size = 1504751 }, - { url = "https://files.pythonhosted.org/packages/fa/3a/5f38667d313e983c432f3fcd86932177519ed8790c724e07d77d1de0188a/kiwisolver-1.4.7-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3da53da805b71e41053dc670f9a820d1157aae77b6b944e08024d17bcd51ef88", size = 1436990 }, - { url = "https://files.pythonhosted.org/packages/cb/3b/1520301a47326e6a6043b502647e42892be33b3f051e9791cc8bb43f1a32/kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8705f17dfeb43139a692298cb6637ee2e59c0194538153e83e9ee0c75c2eddde", size = 2191122 }, - { url = "https://files.pythonhosted.org/packages/cf/c4/eb52da300c166239a2233f1f9c4a1b767dfab98fae27681bfb7ea4873cb6/kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:82a5c2f4b87c26bb1a0ef3d16b5c4753434633b83d365cc0ddf2770c93829e3c", size = 2338126 }, - { url = "https://files.pythonhosted.org/packages/1a/cb/42b92fd5eadd708dd9107c089e817945500685f3437ce1fd387efebc6d6e/kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ce8be0466f4c0d585cdb6c1e2ed07232221df101a4c6f28821d2aa754ca2d9e2", size = 2298313 }, - { url = "https://files.pythonhosted.org/packages/4f/eb/be25aa791fe5fc75a8b1e0c965e00f942496bc04635c9aae8035f6b76dcd/kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:409afdfe1e2e90e6ee7fc896f3df9a7fec8e793e58bfa0d052c8a82f99c37abb", size = 2437784 }, - { url = "https://files.pythonhosted.org/packages/c5/22/30a66be7f3368d76ff95689e1c2e28d382383952964ab15330a15d8bfd03/kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5b9c3f4ee0b9a439d2415012bd1b1cc2df59e4d6a9939f4d669241d30b414327", size = 2253988 }, - { url = "https://files.pythonhosted.org/packages/35/d3/5f2ecb94b5211c8a04f218a76133cc8d6d153b0f9cd0b45fad79907f0689/kiwisolver-1.4.7-cp39-cp39-win32.whl", hash = "sha256:a79ae34384df2b615eefca647a2873842ac3b596418032bef9a7283675962644", size = 46980 }, - { url = "https://files.pythonhosted.org/packages/ef/17/cd10d020578764ea91740204edc6b3236ed8106228a46f568d716b11feb2/kiwisolver-1.4.7-cp39-cp39-win_amd64.whl", hash = "sha256:cf0438b42121a66a3a667de17e779330fc0f20b0d97d59d2f2121e182b0505e4", size = 55847 }, - { url = "https://files.pythonhosted.org/packages/91/84/32232502020bd78d1d12be7afde15811c64a95ed1f606c10456db4e4c3ac/kiwisolver-1.4.7-cp39-cp39-win_arm64.whl", hash = "sha256:764202cc7e70f767dab49e8df52c7455e8de0df5d858fa801a11aa0d882ccf3f", size = 48494 }, - { url = "https://files.pythonhosted.org/packages/ac/59/741b79775d67ab67ced9bb38552da688c0305c16e7ee24bba7a2be253fb7/kiwisolver-1.4.7-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:94252291e3fe68001b1dd747b4c0b3be12582839b95ad4d1b641924d68fd4643", size = 59491 }, - { url = "https://files.pythonhosted.org/packages/58/cc/fb239294c29a5656e99e3527f7369b174dd9cc7c3ef2dea7cb3c54a8737b/kiwisolver-1.4.7-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5b7dfa3b546da08a9f622bb6becdb14b3e24aaa30adba66749d38f3cc7ea9706", size = 57648 }, - { url = "https://files.pythonhosted.org/packages/3b/ef/2f009ac1f7aab9f81efb2d837301d255279d618d27b6015780115ac64bdd/kiwisolver-1.4.7-pp310-pypy310_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd3de6481f4ed8b734da5df134cd5a6a64fe32124fe83dde1e5b5f29fe30b1e6", size = 84257 }, - { url = "https://files.pythonhosted.org/packages/81/e1/c64f50987f85b68b1c52b464bb5bf73e71570c0f7782d626d1eb283ad620/kiwisolver-1.4.7-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a91b5f9f1205845d488c928e8570dcb62b893372f63b8b6e98b863ebd2368ff2", size = 80906 }, - { url = "https://files.pythonhosted.org/packages/fd/71/1687c5c0a0be2cee39a5c9c389e546f9c6e215e46b691d00d9f646892083/kiwisolver-1.4.7-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40fa14dbd66b8b8f470d5fc79c089a66185619d31645f9b0773b88b19f7223c4", size = 79951 }, - { url = "https://files.pythonhosted.org/packages/ea/8b/d7497df4a1cae9367adf21665dd1f896c2a7aeb8769ad77b662c5e2bcce7/kiwisolver-1.4.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:eb542fe7933aa09d8d8f9d9097ef37532a7df6497819d16efe4359890a2f417a", size = 55715 }, - { url = "https://files.pythonhosted.org/packages/d5/df/ce37d9b26f07ab90880923c94d12a6ff4d27447096b4c849bfc4339ccfdf/kiwisolver-1.4.7-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:8b01aac285f91ca889c800042c35ad3b239e704b150cfd3382adfc9dcc780e39", size = 58666 }, - { url = "https://files.pythonhosted.org/packages/b0/d3/e4b04f43bc629ac8e186b77b2b1a251cdfa5b7610fa189dc0db622672ce6/kiwisolver-1.4.7-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:48be928f59a1f5c8207154f935334d374e79f2b5d212826307d072595ad76a2e", size = 57088 }, - { url = "https://files.pythonhosted.org/packages/30/1c/752df58e2d339e670a535514d2db4fe8c842ce459776b8080fbe08ebb98e/kiwisolver-1.4.7-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f37cfe618a117e50d8c240555331160d73d0411422b59b5ee217843d7b693608", size = 84321 }, - { url = "https://files.pythonhosted.org/packages/f0/f8/fe6484e847bc6e238ec9f9828089fb2c0bb53f2f5f3a79351fde5b565e4f/kiwisolver-1.4.7-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:599b5c873c63a1f6ed7eead644a8a380cfbdf5db91dcb6f85707aaab213b1674", size = 80776 }, - { url = "https://files.pythonhosted.org/packages/9b/57/d7163c0379f250ef763aba85330a19feefb5ce6cb541ade853aaba881524/kiwisolver-1.4.7-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:801fa7802e5cfabe3ab0c81a34c323a319b097dfb5004be950482d882f3d7225", size = 79984 }, - { url = "https://files.pythonhosted.org/packages/8c/95/4a103776c265d13b3d2cd24fb0494d4e04ea435a8ef97e1b2c026d43250b/kiwisolver-1.4.7-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:0c6c43471bc764fad4bc99c5c2d6d16a676b1abf844ca7c8702bdae92df01ee0", size = 55811 }, -] - -[[package]] -name = "kiwisolver" -version = "1.4.8" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.11'", - "python_full_version == '3.10.*'", -] -sdist = { url = "https://files.pythonhosted.org/packages/82/59/7c91426a8ac292e1cdd53a63b6d9439abd573c875c3f92c146767dd33faf/kiwisolver-1.4.8.tar.gz", hash = "sha256:23d5f023bdc8c7e54eb65f03ca5d5bb25b601eac4d7f1a042888a1f45237987e", size = 97538 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/47/5f/4d8e9e852d98ecd26cdf8eaf7ed8bc33174033bba5e07001b289f07308fd/kiwisolver-1.4.8-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88c6f252f6816a73b1f8c904f7bbe02fd67c09a69f7cb8a0eecdbf5ce78e63db", size = 124623 }, - { url = "https://files.pythonhosted.org/packages/1d/70/7f5af2a18a76fe92ea14675f8bd88ce53ee79e37900fa5f1a1d8e0b42998/kiwisolver-1.4.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c72941acb7b67138f35b879bbe85be0f6c6a70cab78fe3ef6db9c024d9223e5b", size = 66720 }, - { url = "https://files.pythonhosted.org/packages/c6/13/e15f804a142353aefd089fadc8f1d985561a15358c97aca27b0979cb0785/kiwisolver-1.4.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ce2cf1e5688edcb727fdf7cd1bbd0b6416758996826a8be1d958f91880d0809d", size = 65413 }, - { url = "https://files.pythonhosted.org/packages/ce/6d/67d36c4d2054e83fb875c6b59d0809d5c530de8148846b1370475eeeece9/kiwisolver-1.4.8-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c8bf637892dc6e6aad2bc6d4d69d08764166e5e3f69d469e55427b6ac001b19d", size = 1650826 }, - { url = "https://files.pythonhosted.org/packages/de/c6/7b9bb8044e150d4d1558423a1568e4f227193662a02231064e3824f37e0a/kiwisolver-1.4.8-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:034d2c891f76bd3edbdb3ea11140d8510dca675443da7304205a2eaa45d8334c", size = 1628231 }, - { url = "https://files.pythonhosted.org/packages/b6/38/ad10d437563063eaaedbe2c3540a71101fc7fb07a7e71f855e93ea4de605/kiwisolver-1.4.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d47b28d1dfe0793d5e96bce90835e17edf9a499b53969b03c6c47ea5985844c3", size = 1408938 }, - { url = "https://files.pythonhosted.org/packages/52/ce/c0106b3bd7f9e665c5f5bc1e07cc95b5dabd4e08e3dad42dbe2faad467e7/kiwisolver-1.4.8-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb158fe28ca0c29f2260cca8c43005329ad58452c36f0edf298204de32a9a3ed", size = 1422799 }, - { url = "https://files.pythonhosted.org/packages/d0/87/efb704b1d75dc9758087ba374c0f23d3254505edaedd09cf9d247f7878b9/kiwisolver-1.4.8-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5536185fce131780ebd809f8e623bf4030ce1b161353166c49a3c74c287897f", size = 1354362 }, - { url = "https://files.pythonhosted.org/packages/eb/b3/fd760dc214ec9a8f208b99e42e8f0130ff4b384eca8b29dd0efc62052176/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:369b75d40abedc1da2c1f4de13f3482cb99e3237b38726710f4a793432b1c5ff", size = 2222695 }, - { url = "https://files.pythonhosted.org/packages/a2/09/a27fb36cca3fc01700687cc45dae7a6a5f8eeb5f657b9f710f788748e10d/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:641f2ddf9358c80faa22e22eb4c9f54bd3f0e442e038728f500e3b978d00aa7d", size = 2370802 }, - { url = "https://files.pythonhosted.org/packages/3d/c3/ba0a0346db35fe4dc1f2f2cf8b99362fbb922d7562e5f911f7ce7a7b60fa/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d561d2d8883e0819445cfe58d7ddd673e4015c3c57261d7bdcd3710d0d14005c", size = 2334646 }, - { url = "https://files.pythonhosted.org/packages/41/52/942cf69e562f5ed253ac67d5c92a693745f0bed3c81f49fc0cbebe4d6b00/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:1732e065704b47c9afca7ffa272f845300a4eb959276bf6970dc07265e73b605", size = 2467260 }, - { url = "https://files.pythonhosted.org/packages/32/26/2d9668f30d8a494b0411d4d7d4ea1345ba12deb6a75274d58dd6ea01e951/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bcb1ebc3547619c3b58a39e2448af089ea2ef44b37988caf432447374941574e", size = 2288633 }, - { url = "https://files.pythonhosted.org/packages/98/99/0dd05071654aa44fe5d5e350729961e7bb535372935a45ac89a8924316e6/kiwisolver-1.4.8-cp310-cp310-win_amd64.whl", hash = "sha256:89c107041f7b27844179ea9c85d6da275aa55ecf28413e87624d033cf1f6b751", size = 71885 }, - { url = "https://files.pythonhosted.org/packages/6c/fc/822e532262a97442989335394d441cd1d0448c2e46d26d3e04efca84df22/kiwisolver-1.4.8-cp310-cp310-win_arm64.whl", hash = "sha256:b5773efa2be9eb9fcf5415ea3ab70fc785d598729fd6057bea38d539ead28271", size = 65175 }, - { url = "https://files.pythonhosted.org/packages/da/ed/c913ee28936c371418cb167b128066ffb20bbf37771eecc2c97edf8a6e4c/kiwisolver-1.4.8-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a4d3601908c560bdf880f07d94f31d734afd1bb71e96585cace0e38ef44c6d84", size = 124635 }, - { url = "https://files.pythonhosted.org/packages/4c/45/4a7f896f7467aaf5f56ef093d1f329346f3b594e77c6a3c327b2d415f521/kiwisolver-1.4.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:856b269c4d28a5c0d5e6c1955ec36ebfd1651ac00e1ce0afa3e28da95293b561", size = 66717 }, - { url = "https://files.pythonhosted.org/packages/5f/b4/c12b3ac0852a3a68f94598d4c8d569f55361beef6159dce4e7b624160da2/kiwisolver-1.4.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c2b9a96e0f326205af81a15718a9073328df1173a2619a68553decb7097fd5d7", size = 65413 }, - { url = "https://files.pythonhosted.org/packages/a9/98/1df4089b1ed23d83d410adfdc5947245c753bddfbe06541c4aae330e9e70/kiwisolver-1.4.8-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c5020c83e8553f770cb3b5fc13faac40f17e0b205bd237aebd21d53d733adb03", size = 1343994 }, - { url = "https://files.pythonhosted.org/packages/8d/bf/b4b169b050c8421a7c53ea1ea74e4ef9c335ee9013216c558a047f162d20/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dace81d28c787956bfbfbbfd72fdcef014f37d9b48830829e488fdb32b49d954", size = 1434804 }, - { url = "https://files.pythonhosted.org/packages/66/5a/e13bd341fbcf73325ea60fdc8af752addf75c5079867af2e04cc41f34434/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:11e1022b524bd48ae56c9b4f9296bce77e15a2e42a502cceba602f804b32bb79", size = 1450690 }, - { url = "https://files.pythonhosted.org/packages/9b/4f/5955dcb376ba4a830384cc6fab7d7547bd6759fe75a09564910e9e3bb8ea/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b9b4d2892fefc886f30301cdd80debd8bb01ecdf165a449eb6e78f79f0fabd6", size = 1376839 }, - { url = "https://files.pythonhosted.org/packages/3a/97/5edbed69a9d0caa2e4aa616ae7df8127e10f6586940aa683a496c2c280b9/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a96c0e790ee875d65e340ab383700e2b4891677b7fcd30a699146f9384a2bb0", size = 1435109 }, - { url = "https://files.pythonhosted.org/packages/13/fc/e756382cb64e556af6c1809a1bbb22c141bbc2445049f2da06b420fe52bf/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:23454ff084b07ac54ca8be535f4174170c1094a4cff78fbae4f73a4bcc0d4dab", size = 2245269 }, - { url = "https://files.pythonhosted.org/packages/76/15/e59e45829d7f41c776d138245cabae6515cb4eb44b418f6d4109c478b481/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:87b287251ad6488e95b4f0b4a79a6d04d3ea35fde6340eb38fbd1ca9cd35bbbc", size = 2393468 }, - { url = "https://files.pythonhosted.org/packages/e9/39/483558c2a913ab8384d6e4b66a932406f87c95a6080112433da5ed668559/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b21dbe165081142b1232a240fc6383fd32cdd877ca6cc89eab93e5f5883e1c25", size = 2355394 }, - { url = "https://files.pythonhosted.org/packages/01/aa/efad1fbca6570a161d29224f14b082960c7e08268a133fe5dc0f6906820e/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:768cade2c2df13db52475bd28d3a3fac8c9eff04b0e9e2fda0f3760f20b3f7fc", size = 2490901 }, - { url = "https://files.pythonhosted.org/packages/c9/4f/15988966ba46bcd5ab9d0c8296914436720dd67fca689ae1a75b4ec1c72f/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d47cfb2650f0e103d4bf68b0b5804c68da97272c84bb12850d877a95c056bd67", size = 2312306 }, - { url = "https://files.pythonhosted.org/packages/2d/27/bdf1c769c83f74d98cbc34483a972f221440703054894a37d174fba8aa68/kiwisolver-1.4.8-cp311-cp311-win_amd64.whl", hash = "sha256:ed33ca2002a779a2e20eeb06aea7721b6e47f2d4b8a8ece979d8ba9e2a167e34", size = 71966 }, - { url = "https://files.pythonhosted.org/packages/4a/c9/9642ea855604aeb2968a8e145fc662edf61db7632ad2e4fb92424be6b6c0/kiwisolver-1.4.8-cp311-cp311-win_arm64.whl", hash = "sha256:16523b40aab60426ffdebe33ac374457cf62863e330a90a0383639ce14bf44b2", size = 65311 }, - { url = "https://files.pythonhosted.org/packages/fc/aa/cea685c4ab647f349c3bc92d2daf7ae34c8e8cf405a6dcd3a497f58a2ac3/kiwisolver-1.4.8-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d6af5e8815fd02997cb6ad9bbed0ee1e60014438ee1a5c2444c96f87b8843502", size = 124152 }, - { url = "https://files.pythonhosted.org/packages/c5/0b/8db6d2e2452d60d5ebc4ce4b204feeb16176a851fd42462f66ade6808084/kiwisolver-1.4.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bade438f86e21d91e0cf5dd7c0ed00cda0f77c8c1616bd83f9fc157fa6760d31", size = 66555 }, - { url = "https://files.pythonhosted.org/packages/60/26/d6a0db6785dd35d3ba5bf2b2df0aedc5af089962c6eb2cbf67a15b81369e/kiwisolver-1.4.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b83dc6769ddbc57613280118fb4ce3cd08899cc3369f7d0e0fab518a7cf37fdb", size = 65067 }, - { url = "https://files.pythonhosted.org/packages/c9/ed/1d97f7e3561e09757a196231edccc1bcf59d55ddccefa2afc9c615abd8e0/kiwisolver-1.4.8-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:111793b232842991be367ed828076b03d96202c19221b5ebab421ce8bcad016f", size = 1378443 }, - { url = "https://files.pythonhosted.org/packages/29/61/39d30b99954e6b46f760e6289c12fede2ab96a254c443639052d1b573fbc/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:257af1622860e51b1a9d0ce387bf5c2c4f36a90594cb9514f55b074bcc787cfc", size = 1472728 }, - { url = "https://files.pythonhosted.org/packages/0c/3e/804163b932f7603ef256e4a715e5843a9600802bb23a68b4e08c8c0ff61d/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:69b5637c3f316cab1ec1c9a12b8c5f4750a4c4b71af9157645bf32830e39c03a", size = 1478388 }, - { url = "https://files.pythonhosted.org/packages/8a/9e/60eaa75169a154700be74f875a4d9961b11ba048bef315fbe89cb6999056/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:782bb86f245ec18009890e7cb8d13a5ef54dcf2ebe18ed65f795e635a96a1c6a", size = 1413849 }, - { url = "https://files.pythonhosted.org/packages/bc/b3/9458adb9472e61a998c8c4d95cfdfec91c73c53a375b30b1428310f923e4/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc978a80a0db3a66d25767b03688f1147a69e6237175c0f4ffffaaedf744055a", size = 1475533 }, - { url = "https://files.pythonhosted.org/packages/e4/7a/0a42d9571e35798de80aef4bb43a9b672aa7f8e58643d7bd1950398ffb0a/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:36dbbfd34838500a31f52c9786990d00150860e46cd5041386f217101350f0d3", size = 2268898 }, - { url = "https://files.pythonhosted.org/packages/d9/07/1255dc8d80271400126ed8db35a1795b1a2c098ac3a72645075d06fe5c5d/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:eaa973f1e05131de5ff3569bbba7f5fd07ea0595d3870ed4a526d486fe57fa1b", size = 2425605 }, - { url = "https://files.pythonhosted.org/packages/84/df/5a3b4cf13780ef6f6942df67b138b03b7e79e9f1f08f57c49957d5867f6e/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a66f60f8d0c87ab7f59b6fb80e642ebb29fec354a4dfad687ca4092ae69d04f4", size = 2375801 }, - { url = "https://files.pythonhosted.org/packages/8f/10/2348d068e8b0f635c8c86892788dac7a6b5c0cb12356620ab575775aad89/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:858416b7fb777a53f0c59ca08190ce24e9abbd3cffa18886a5781b8e3e26f65d", size = 2520077 }, - { url = "https://files.pythonhosted.org/packages/32/d8/014b89fee5d4dce157d814303b0fce4d31385a2af4c41fed194b173b81ac/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:085940635c62697391baafaaeabdf3dd7a6c3643577dde337f4d66eba021b2b8", size = 2338410 }, - { url = "https://files.pythonhosted.org/packages/bd/72/dfff0cc97f2a0776e1c9eb5bef1ddfd45f46246c6533b0191887a427bca5/kiwisolver-1.4.8-cp312-cp312-win_amd64.whl", hash = "sha256:01c3d31902c7db5fb6182832713d3b4122ad9317c2c5877d0539227d96bb2e50", size = 71853 }, - { url = "https://files.pythonhosted.org/packages/dc/85/220d13d914485c0948a00f0b9eb419efaf6da81b7d72e88ce2391f7aed8d/kiwisolver-1.4.8-cp312-cp312-win_arm64.whl", hash = "sha256:a3c44cb68861de93f0c4a8175fbaa691f0aa22550c331fefef02b618a9dcb476", size = 65424 }, - { url = "https://files.pythonhosted.org/packages/79/b3/e62464a652f4f8cd9006e13d07abad844a47df1e6537f73ddfbf1bc997ec/kiwisolver-1.4.8-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:1c8ceb754339793c24aee1c9fb2485b5b1f5bb1c2c214ff13368431e51fc9a09", size = 124156 }, - { url = "https://files.pythonhosted.org/packages/8d/2d/f13d06998b546a2ad4f48607a146e045bbe48030774de29f90bdc573df15/kiwisolver-1.4.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a62808ac74b5e55a04a408cda6156f986cefbcf0ada13572696b507cc92fa1", size = 66555 }, - { url = "https://files.pythonhosted.org/packages/59/e3/b8bd14b0a54998a9fd1e8da591c60998dc003618cb19a3f94cb233ec1511/kiwisolver-1.4.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:68269e60ee4929893aad82666821aaacbd455284124817af45c11e50a4b42e3c", size = 65071 }, - { url = "https://files.pythonhosted.org/packages/f0/1c/6c86f6d85ffe4d0ce04228d976f00674f1df5dc893bf2dd4f1928748f187/kiwisolver-1.4.8-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:34d142fba9c464bc3bbfeff15c96eab0e7310343d6aefb62a79d51421fcc5f1b", size = 1378053 }, - { url = "https://files.pythonhosted.org/packages/4e/b9/1c6e9f6dcb103ac5cf87cb695845f5fa71379021500153566d8a8a9fc291/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ddc373e0eef45b59197de815b1b28ef89ae3955e7722cc9710fb91cd77b7f47", size = 1472278 }, - { url = "https://files.pythonhosted.org/packages/ee/81/aca1eb176de671f8bda479b11acdc42c132b61a2ac861c883907dde6debb/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:77e6f57a20b9bd4e1e2cedda4d0b986ebd0216236f0106e55c28aea3d3d69b16", size = 1478139 }, - { url = "https://files.pythonhosted.org/packages/49/f4/e081522473671c97b2687d380e9e4c26f748a86363ce5af48b4a28e48d06/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08e77738ed7538f036cd1170cbed942ef749137b1311fa2bbe2a7fda2f6bf3cc", size = 1413517 }, - { url = "https://files.pythonhosted.org/packages/8f/e9/6a7d025d8da8c4931522922cd706105aa32b3291d1add8c5427cdcd66e63/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5ce1e481a74b44dd5e92ff03ea0cb371ae7a0268318e202be06c8f04f4f1246", size = 1474952 }, - { url = "https://files.pythonhosted.org/packages/82/13/13fa685ae167bee5d94b415991c4fc7bb0a1b6ebea6e753a87044b209678/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fc2ace710ba7c1dfd1a3b42530b62b9ceed115f19a1656adefce7b1782a37794", size = 2269132 }, - { url = "https://files.pythonhosted.org/packages/ef/92/bb7c9395489b99a6cb41d502d3686bac692586db2045adc19e45ee64ed23/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:3452046c37c7692bd52b0e752b87954ef86ee2224e624ef7ce6cb21e8c41cc1b", size = 2425997 }, - { url = "https://files.pythonhosted.org/packages/ed/12/87f0e9271e2b63d35d0d8524954145837dd1a6c15b62a2d8c1ebe0f182b4/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7e9a60b50fe8b2ec6f448fe8d81b07e40141bfced7f896309df271a0b92f80f3", size = 2376060 }, - { url = "https://files.pythonhosted.org/packages/02/6e/c8af39288edbce8bf0fa35dee427b082758a4b71e9c91ef18fa667782138/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:918139571133f366e8362fa4a297aeba86c7816b7ecf0bc79168080e2bd79957", size = 2520471 }, - { url = "https://files.pythonhosted.org/packages/13/78/df381bc7b26e535c91469f77f16adcd073beb3e2dd25042efd064af82323/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e063ef9f89885a1d68dd8b2e18f5ead48653176d10a0e324e3b0030e3a69adeb", size = 2338793 }, - { url = "https://files.pythonhosted.org/packages/d0/dc/c1abe38c37c071d0fc71c9a474fd0b9ede05d42f5a458d584619cfd2371a/kiwisolver-1.4.8-cp313-cp313-win_amd64.whl", hash = "sha256:a17b7c4f5b2c51bb68ed379defd608a03954a1845dfed7cc0117f1cc8a9b7fd2", size = 71855 }, - { url = "https://files.pythonhosted.org/packages/a0/b6/21529d595b126ac298fdd90b705d87d4c5693de60023e0efcb4f387ed99e/kiwisolver-1.4.8-cp313-cp313-win_arm64.whl", hash = "sha256:3cd3bc628b25f74aedc6d374d5babf0166a92ff1317f46267f12d2ed54bc1d30", size = 65430 }, - { url = "https://files.pythonhosted.org/packages/34/bd/b89380b7298e3af9b39f49334e3e2a4af0e04819789f04b43d560516c0c8/kiwisolver-1.4.8-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:370fd2df41660ed4e26b8c9d6bbcad668fbe2560462cba151a721d49e5b6628c", size = 126294 }, - { url = "https://files.pythonhosted.org/packages/83/41/5857dc72e5e4148eaac5aa76e0703e594e4465f8ab7ec0fc60e3a9bb8fea/kiwisolver-1.4.8-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:84a2f830d42707de1d191b9490ac186bf7997a9495d4e9072210a1296345f7dc", size = 67736 }, - { url = "https://files.pythonhosted.org/packages/e1/d1/be059b8db56ac270489fb0b3297fd1e53d195ba76e9bbb30e5401fa6b759/kiwisolver-1.4.8-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7a3ad337add5148cf51ce0b55642dc551c0b9d6248458a757f98796ca7348712", size = 66194 }, - { url = "https://files.pythonhosted.org/packages/e1/83/4b73975f149819eb7dcf9299ed467eba068ecb16439a98990dcb12e63fdd/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7506488470f41169b86d8c9aeff587293f530a23a23a49d6bc64dab66bedc71e", size = 1465942 }, - { url = "https://files.pythonhosted.org/packages/c7/2c/30a5cdde5102958e602c07466bce058b9d7cb48734aa7a4327261ac8e002/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f0121b07b356a22fb0414cec4666bbe36fd6d0d759db3d37228f496ed67c880", size = 1595341 }, - { url = "https://files.pythonhosted.org/packages/ff/9b/1e71db1c000385aa069704f5990574b8244cce854ecd83119c19e83c9586/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d6d6bd87df62c27d4185de7c511c6248040afae67028a8a22012b010bc7ad062", size = 1598455 }, - { url = "https://files.pythonhosted.org/packages/85/92/c8fec52ddf06231b31cbb779af77e99b8253cd96bd135250b9498144c78b/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:291331973c64bb9cce50bbe871fb2e675c4331dab4f31abe89f175ad7679a4d7", size = 1522138 }, - { url = "https://files.pythonhosted.org/packages/0b/51/9eb7e2cd07a15d8bdd976f6190c0164f92ce1904e5c0c79198c4972926b7/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:893f5525bb92d3d735878ec00f781b2de998333659507d29ea4466208df37bed", size = 1582857 }, - { url = "https://files.pythonhosted.org/packages/0f/95/c5a00387a5405e68ba32cc64af65ce881a39b98d73cc394b24143bebc5b8/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b47a465040146981dc9db8647981b8cb96366fbc8d452b031e4f8fdffec3f26d", size = 2293129 }, - { url = "https://files.pythonhosted.org/packages/44/83/eeb7af7d706b8347548313fa3a3a15931f404533cc54fe01f39e830dd231/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:99cea8b9dd34ff80c521aef46a1dddb0dcc0283cf18bde6d756f1e6f31772165", size = 2421538 }, - { url = "https://files.pythonhosted.org/packages/05/f9/27e94c1b3eb29e6933b6986ffc5fa1177d2cd1f0c8efc5f02c91c9ac61de/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:151dffc4865e5fe6dafce5480fab84f950d14566c480c08a53c663a0020504b6", size = 2390661 }, - { url = "https://files.pythonhosted.org/packages/d9/d4/3c9735faa36ac591a4afcc2980d2691000506050b7a7e80bcfe44048daa7/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:577facaa411c10421314598b50413aa1ebcf5126f704f1e5d72d7e4e9f020d90", size = 2546710 }, - { url = "https://files.pythonhosted.org/packages/4c/fa/be89a49c640930180657482a74970cdcf6f7072c8d2471e1babe17a222dc/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:be4816dc51c8a471749d664161b434912eee82f2ea66bd7628bd14583a833e85", size = 2349213 }, - { url = "https://files.pythonhosted.org/packages/1f/f9/ae81c47a43e33b93b0a9819cac6723257f5da2a5a60daf46aa5c7226ea85/kiwisolver-1.4.8-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:e7a019419b7b510f0f7c9dceff8c5eae2392037eae483a7f9162625233802b0a", size = 60403 }, - { url = "https://files.pythonhosted.org/packages/58/ca/f92b5cb6f4ce0c1ebfcfe3e2e42b96917e16f7090e45b21102941924f18f/kiwisolver-1.4.8-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:286b18e86682fd2217a48fc6be6b0f20c1d0ed10958d8dc53453ad58d7be0bf8", size = 58657 }, - { url = "https://files.pythonhosted.org/packages/80/28/ae0240f732f0484d3a4dc885d055653c47144bdf59b670aae0ec3c65a7c8/kiwisolver-1.4.8-pp310-pypy310_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4191ee8dfd0be1c3666ccbac178c5a05d5f8d689bbe3fc92f3c4abec817f8fe0", size = 84948 }, - { url = "https://files.pythonhosted.org/packages/5d/eb/78d50346c51db22c7203c1611f9b513075f35c4e0e4877c5dde378d66043/kiwisolver-1.4.8-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7cd2785b9391f2873ad46088ed7599a6a71e762e1ea33e87514b1a441ed1da1c", size = 81186 }, - { url = "https://files.pythonhosted.org/packages/43/f8/7259f18c77adca88d5f64f9a522792e178b2691f3748817a8750c2d216ef/kiwisolver-1.4.8-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c07b29089b7ba090b6f1a669f1411f27221c3662b3a1b7010e67b59bb5a6f10b", size = 80279 }, - { url = "https://files.pythonhosted.org/packages/3a/1d/50ad811d1c5dae091e4cf046beba925bcae0a610e79ae4c538f996f63ed5/kiwisolver-1.4.8-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:65ea09a5a3faadd59c2ce96dc7bf0f364986a315949dc6374f04396b0d60e09b", size = 71762 }, -] - [[package]] name = "llvmlite" version = "0.43.0" @@ -695,125 +279,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d0/81/e66fc86539293282fd9cb7c9417438e897f369e79ffb62e1ae5e5154d4dd/llvmlite-0.44.0-cp313-cp313-win_amd64.whl", hash = "sha256:2fb7c4f2fb86cbae6dca3db9ab203eeea0e22d73b99bc2341cdf9de93612e930", size = 30331193 }, ] -[[package]] -name = "matplotlib" -version = "3.9.4" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10'", -] -dependencies = [ - { name = "contourpy", version = "1.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "cycler", marker = "python_full_version < '3.10'" }, - { name = "fonttools", marker = "python_full_version < '3.10'" }, - { name = "importlib-resources", marker = "python_full_version < '3.10'" }, - { name = "kiwisolver", version = "1.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "packaging", marker = "python_full_version < '3.10'" }, - { name = "pillow", marker = "python_full_version < '3.10'" }, - { name = "pyparsing", marker = "python_full_version < '3.10'" }, - { name = "python-dateutil", marker = "python_full_version < '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/df/17/1747b4154034befd0ed33b52538f5eb7752d05bb51c5e2a31470c3bc7d52/matplotlib-3.9.4.tar.gz", hash = "sha256:1e00e8be7393cbdc6fedfa8a6fba02cf3e83814b285db1c60b906a023ba41bc3", size = 36106529 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/94/27d2e2c30d54b56c7b764acc1874a909e34d1965a427fc7092bb6a588b63/matplotlib-3.9.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:c5fdd7abfb706dfa8d307af64a87f1a862879ec3cd8d0ec8637458f0885b9c50", size = 7885089 }, - { url = "https://files.pythonhosted.org/packages/c6/25/828273307e40a68eb8e9df832b6b2aaad075864fdc1de4b1b81e40b09e48/matplotlib-3.9.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d89bc4e85e40a71d1477780366c27fb7c6494d293e1617788986f74e2a03d7ff", size = 7770600 }, - { url = "https://files.pythonhosted.org/packages/f2/65/f841a422ec994da5123368d76b126acf4fc02ea7459b6e37c4891b555b83/matplotlib-3.9.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ddf9f3c26aae695c5daafbf6b94e4c1a30d6cd617ba594bbbded3b33a1fcfa26", size = 8200138 }, - { url = "https://files.pythonhosted.org/packages/07/06/272aca07a38804d93b6050813de41ca7ab0e29ba7a9dd098e12037c919a9/matplotlib-3.9.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18ebcf248030173b59a868fda1fe42397253f6698995b55e81e1f57431d85e50", size = 8312711 }, - { url = "https://files.pythonhosted.org/packages/98/37/f13e23b233c526b7e27ad61be0a771894a079e0f7494a10d8d81557e0e9a/matplotlib-3.9.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:974896ec43c672ec23f3f8c648981e8bc880ee163146e0312a9b8def2fac66f5", size = 9090622 }, - { url = "https://files.pythonhosted.org/packages/4f/8c/b1f5bd2bd70e60f93b1b54c4d5ba7a992312021d0ddddf572f9a1a6d9348/matplotlib-3.9.4-cp310-cp310-win_amd64.whl", hash = "sha256:4598c394ae9711cec135639374e70871fa36b56afae17bdf032a345be552a88d", size = 7828211 }, - { url = "https://files.pythonhosted.org/packages/74/4b/65be7959a8fa118a3929b49a842de5b78bb55475236fcf64f3e308ff74a0/matplotlib-3.9.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d4dd29641d9fb8bc4492420c5480398dd40a09afd73aebe4eb9d0071a05fbe0c", size = 7894430 }, - { url = "https://files.pythonhosted.org/packages/e9/18/80f70d91896e0a517b4a051c3fd540daa131630fd75e02e250365353b253/matplotlib-3.9.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30e5b22e8bcfb95442bf7d48b0d7f3bdf4a450cbf68986ea45fca3d11ae9d099", size = 7780045 }, - { url = "https://files.pythonhosted.org/packages/a2/73/ccb381026e3238c5c25c3609ba4157b2d1a617ec98d65a8b4ee4e1e74d02/matplotlib-3.9.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bb0030d1d447fd56dcc23b4c64a26e44e898f0416276cac1ebc25522e0ac249", size = 8209906 }, - { url = "https://files.pythonhosted.org/packages/ab/33/1648da77b74741c89f5ea95cbf42a291b4b364f2660b316318811404ed97/matplotlib-3.9.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aca90ed222ac3565d2752b83dbb27627480d27662671e4d39da72e97f657a423", size = 8322873 }, - { url = "https://files.pythonhosted.org/packages/57/d3/8447ba78bc6593c9044c372d1609f8ea10fb1e071e7a9e0747bea74fc16c/matplotlib-3.9.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a181b2aa2906c608fcae72f977a4a2d76e385578939891b91c2550c39ecf361e", size = 9099566 }, - { url = "https://files.pythonhosted.org/packages/23/e1/4f0e237bf349c02ff9d1b6e7109f1a17f745263809b9714a8576dc17752b/matplotlib-3.9.4-cp311-cp311-win_amd64.whl", hash = "sha256:1f6882828231eca17f501c4dcd98a05abb3f03d157fbc0769c6911fe08b6cfd3", size = 7838065 }, - { url = "https://files.pythonhosted.org/packages/1a/2b/c918bf6c19d6445d1cefe3d2e42cb740fb997e14ab19d4daeb6a7ab8a157/matplotlib-3.9.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:dfc48d67e6661378a21c2983200a654b72b5c5cdbd5d2cf6e5e1ece860f0cc70", size = 7891131 }, - { url = "https://files.pythonhosted.org/packages/c1/e5/b4e8fc601ca302afeeabf45f30e706a445c7979a180e3a978b78b2b681a4/matplotlib-3.9.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:47aef0fab8332d02d68e786eba8113ffd6f862182ea2999379dec9e237b7e483", size = 7776365 }, - { url = "https://files.pythonhosted.org/packages/99/06/b991886c506506476e5d83625c5970c656a491b9f80161458fed94597808/matplotlib-3.9.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fba1f52c6b7dc764097f52fd9ab627b90db452c9feb653a59945de16752e965f", size = 8200707 }, - { url = "https://files.pythonhosted.org/packages/c3/e2/556b627498cb27e61026f2d1ba86a78ad1b836fef0996bef5440e8bc9559/matplotlib-3.9.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:173ac3748acaac21afcc3fa1633924609ba1b87749006bc25051c52c422a5d00", size = 8313761 }, - { url = "https://files.pythonhosted.org/packages/58/ff/165af33ec766ff818306ea88e91f9f60d2a6ed543be1eb122a98acbf3b0d/matplotlib-3.9.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:320edea0cadc07007765e33f878b13b3738ffa9745c5f707705692df70ffe0e0", size = 9095284 }, - { url = "https://files.pythonhosted.org/packages/9f/8b/3d0c7a002db3b1ed702731c2a9a06d78d035f1f2fb0fb936a8e43cc1e9f4/matplotlib-3.9.4-cp312-cp312-win_amd64.whl", hash = "sha256:a4a4cfc82330b27042a7169533da7991e8789d180dd5b3daeaee57d75cd5a03b", size = 7841160 }, - { url = "https://files.pythonhosted.org/packages/49/b1/999f89a7556d101b23a2f0b54f1b6e140d73f56804da1398f2f0bc0924bc/matplotlib-3.9.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:37eeffeeca3c940985b80f5b9a7b95ea35671e0e7405001f249848d2b62351b6", size = 7891499 }, - { url = "https://files.pythonhosted.org/packages/87/7b/06a32b13a684977653396a1bfcd34d4e7539c5d55c8cbfaa8ae04d47e4a9/matplotlib-3.9.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3e7465ac859ee4abcb0d836137cd8414e7bb7ad330d905abced457217d4f0f45", size = 7776802 }, - { url = "https://files.pythonhosted.org/packages/65/87/ac498451aff739e515891bbb92e566f3c7ef31891aaa878402a71f9b0910/matplotlib-3.9.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4c12302c34afa0cf061bea23b331e747e5e554b0fa595c96e01c7b75bc3b858", size = 8200802 }, - { url = "https://files.pythonhosted.org/packages/f8/6b/9eb761c00e1cb838f6c92e5f25dcda3f56a87a52f6cb8fdfa561e6cf6a13/matplotlib-3.9.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b8c97917f21b75e72108b97707ba3d48f171541a74aa2a56df7a40626bafc64", size = 8313880 }, - { url = "https://files.pythonhosted.org/packages/d7/a2/c8eaa600e2085eec7e38cbbcc58a30fc78f8224939d31d3152bdafc01fd1/matplotlib-3.9.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0229803bd7e19271b03cb09f27db76c918c467aa4ce2ae168171bc67c3f508df", size = 9094637 }, - { url = "https://files.pythonhosted.org/packages/71/1f/c6e1daea55b7bfeb3d84c6cb1abc449f6a02b181e7e2a5e4db34c3afb793/matplotlib-3.9.4-cp313-cp313-win_amd64.whl", hash = "sha256:7c0d8ef442ebf56ff5e206f8083d08252ee738e04f3dc88ea882853a05488799", size = 7841311 }, - { url = "https://files.pythonhosted.org/packages/c0/3a/2757d3f7d388b14dd48f5a83bea65b6d69f000e86b8f28f74d86e0d375bd/matplotlib-3.9.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a04c3b00066a688834356d196136349cb32f5e1003c55ac419e91585168b88fb", size = 7919989 }, - { url = "https://files.pythonhosted.org/packages/24/28/f5077c79a4f521589a37fe1062d6a6ea3534e068213f7357e7cfffc2e17a/matplotlib-3.9.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:04c519587f6c210626741a1e9a68eefc05966ede24205db8982841826af5871a", size = 7809417 }, - { url = "https://files.pythonhosted.org/packages/36/c8/c523fd2963156692916a8eb7d4069084cf729359f7955cf09075deddfeaf/matplotlib-3.9.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:308afbf1a228b8b525fcd5cec17f246bbbb63b175a3ef6eb7b4d33287ca0cf0c", size = 8226258 }, - { url = "https://files.pythonhosted.org/packages/f6/88/499bf4b8fa9349b6f5c0cf4cead0ebe5da9d67769129f1b5651e5ac51fbc/matplotlib-3.9.4-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ddb3b02246ddcffd3ce98e88fed5b238bc5faff10dbbaa42090ea13241d15764", size = 8335849 }, - { url = "https://files.pythonhosted.org/packages/b8/9f/20a4156b9726188646a030774ee337d5ff695a965be45ce4dbcb9312c170/matplotlib-3.9.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8a75287e9cb9eee48cb79ec1d806f75b29c0fde978cb7223a1f4c5848d696041", size = 9102152 }, - { url = "https://files.pythonhosted.org/packages/10/11/237f9c3a4e8d810b1759b67ff2da7c32c04f9c80aa475e7beb36ed43a8fb/matplotlib-3.9.4-cp313-cp313t-win_amd64.whl", hash = "sha256:488deb7af140f0ba86da003e66e10d55ff915e152c78b4b66d231638400b1965", size = 7896987 }, - { url = "https://files.pythonhosted.org/packages/56/eb/501b465c9fef28f158e414ea3a417913dc2ac748564c7ed41535f23445b4/matplotlib-3.9.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:3c3724d89a387ddf78ff88d2a30ca78ac2b4c89cf37f2db4bd453c34799e933c", size = 7885919 }, - { url = "https://files.pythonhosted.org/packages/da/36/236fbd868b6c91309a5206bd90c3f881f4f44b2d997cd1d6239ef652f878/matplotlib-3.9.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d5f0a8430ffe23d7e32cfd86445864ccad141797f7d25b7c41759a5b5d17cfd7", size = 7771486 }, - { url = "https://files.pythonhosted.org/packages/e0/4b/105caf2d54d5ed11d9f4335398f5103001a03515f2126c936a752ccf1461/matplotlib-3.9.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6bb0141a21aef3b64b633dc4d16cbd5fc538b727e4958be82a0e1c92a234160e", size = 8201838 }, - { url = "https://files.pythonhosted.org/packages/5d/a7/bb01188fb4013d34d274caf44a2f8091255b0497438e8b6c0a7c1710c692/matplotlib-3.9.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:57aa235109e9eed52e2c2949db17da185383fa71083c00c6c143a60e07e0888c", size = 8314492 }, - { url = "https://files.pythonhosted.org/packages/33/19/02e1a37f7141fc605b193e927d0a9cdf9dc124a20b9e68793f4ffea19695/matplotlib-3.9.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b18c600061477ccfdd1e6fd050c33d8be82431700f3452b297a56d9ed7037abb", size = 9092500 }, - { url = "https://files.pythonhosted.org/packages/57/68/c2feb4667adbf882ffa4b3e0ac9967f848980d9f8b5bebd86644aa67ce6a/matplotlib-3.9.4-cp39-cp39-win_amd64.whl", hash = "sha256:ef5f2d1b67d2d2145ff75e10f8c008bfbf71d45137c4b648c87193e7dd053eac", size = 7822962 }, - { url = "https://files.pythonhosted.org/packages/0c/22/2ef6a364cd3f565442b0b055e0599744f1e4314ec7326cdaaa48a4d864d7/matplotlib-3.9.4-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:44e0ed786d769d85bc787b0606a53f2d8d2d1d3c8a2608237365e9121c1a338c", size = 7877995 }, - { url = "https://files.pythonhosted.org/packages/87/b8/2737456e566e9f4d94ae76b8aa0d953d9acb847714f9a7ad80184474f5be/matplotlib-3.9.4-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:09debb9ce941eb23ecdbe7eab972b1c3e0276dcf01688073faff7b0f61d6c6ca", size = 7769300 }, - { url = "https://files.pythonhosted.org/packages/b2/1f/e709c6ec7b5321e6568769baa288c7178e60a93a9da9e682b39450da0e29/matplotlib-3.9.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bcc53cf157a657bfd03afab14774d54ba73aa84d42cfe2480c91bd94873952db", size = 8313423 }, - { url = "https://files.pythonhosted.org/packages/5e/b6/5a1f868782cd13f053a679984e222007ecff654a9bfbac6b27a65f4eeb05/matplotlib-3.9.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:ad45da51be7ad02387801fd154ef74d942f49fe3fcd26a64c94842ba7ec0d865", size = 7854624 }, -] - -[[package]] -name = "matplotlib" -version = "3.10.3" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.11'", - "python_full_version == '3.10.*'", -] -dependencies = [ - { name = "contourpy", version = "1.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "cycler", marker = "python_full_version >= '3.10'" }, - { name = "fonttools", marker = "python_full_version >= '3.10'" }, - { name = "kiwisolver", version = "1.4.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "packaging", marker = "python_full_version >= '3.10'" }, - { name = "pillow", marker = "python_full_version >= '3.10'" }, - { name = "pyparsing", marker = "python_full_version >= '3.10'" }, - { name = "python-dateutil", marker = "python_full_version >= '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/26/91/d49359a21893183ed2a5b6c76bec40e0b1dcbf8ca148f864d134897cfc75/matplotlib-3.10.3.tar.gz", hash = "sha256:2f82d2c5bb7ae93aaaa4cd42aca65d76ce6376f83304fa3a630b569aca274df0", size = 34799811 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d0/ea/2bba25d289d389c7451f331ecd593944b3705f06ddf593fa7be75037d308/matplotlib-3.10.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:213fadd6348d106ca7db99e113f1bea1e65e383c3ba76e8556ba4a3054b65ae7", size = 8167862 }, - { url = "https://files.pythonhosted.org/packages/41/81/cc70b5138c926604e8c9ed810ed4c79e8116ba72e02230852f5c12c87ba2/matplotlib-3.10.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d3bec61cb8221f0ca6313889308326e7bb303d0d302c5cc9e523b2f2e6c73deb", size = 8042149 }, - { url = "https://files.pythonhosted.org/packages/4a/9a/0ff45b6bfa42bb16de597e6058edf2361c298ad5ef93b327728145161bbf/matplotlib-3.10.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c21ae75651c0231b3ba014b6d5e08fb969c40cdb5a011e33e99ed0c9ea86ecb", size = 8453719 }, - { url = "https://files.pythonhosted.org/packages/85/c7/1866e972fed6d71ef136efbc980d4d1854ab7ef1ea8152bbd995ca231c81/matplotlib-3.10.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a49e39755580b08e30e3620efc659330eac5d6534ab7eae50fa5e31f53ee4e30", size = 8590801 }, - { url = "https://files.pythonhosted.org/packages/5d/b9/748f6626d534ab7e255bdc39dc22634d337cf3ce200f261b5d65742044a1/matplotlib-3.10.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cf4636203e1190871d3a73664dea03d26fb019b66692cbfd642faafdad6208e8", size = 9402111 }, - { url = "https://files.pythonhosted.org/packages/1f/78/8bf07bd8fb67ea5665a6af188e70b57fcb2ab67057daa06b85a08e59160a/matplotlib-3.10.3-cp310-cp310-win_amd64.whl", hash = "sha256:fd5641a9bb9d55f4dd2afe897a53b537c834b9012684c8444cc105895c8c16fd", size = 8057213 }, - { url = "https://files.pythonhosted.org/packages/f5/bd/af9f655456f60fe1d575f54fb14704ee299b16e999704817a7645dfce6b0/matplotlib-3.10.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0ef061f74cd488586f552d0c336b2f078d43bc00dc473d2c3e7bfee2272f3fa8", size = 8178873 }, - { url = "https://files.pythonhosted.org/packages/c2/86/e1c86690610661cd716eda5f9d0b35eaf606ae6c9b6736687cfc8f2d0cd8/matplotlib-3.10.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d96985d14dc5f4a736bbea4b9de9afaa735f8a0fc2ca75be2fa9e96b2097369d", size = 8052205 }, - { url = "https://files.pythonhosted.org/packages/54/51/a9f8e49af3883dacddb2da1af5fca1f7468677f1188936452dd9aaaeb9ed/matplotlib-3.10.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7c5f0283da91e9522bdba4d6583ed9d5521566f63729ffb68334f86d0bb98049", size = 8465823 }, - { url = "https://files.pythonhosted.org/packages/e7/e3/c82963a3b86d6e6d5874cbeaa390166458a7f1961bab9feb14d3d1a10f02/matplotlib-3.10.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdfa07c0ec58035242bc8b2c8aae37037c9a886370eef6850703d7583e19964b", size = 8606464 }, - { url = "https://files.pythonhosted.org/packages/0e/34/24da1027e7fcdd9e82da3194c470143c551852757a4b473a09a012f5b945/matplotlib-3.10.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c0b9849a17bce080a16ebcb80a7b714b5677d0ec32161a2cc0a8e5a6030ae220", size = 9413103 }, - { url = "https://files.pythonhosted.org/packages/a6/da/948a017c3ea13fd4a97afad5fdebe2f5bbc4d28c0654510ce6fd6b06b7bd/matplotlib-3.10.3-cp311-cp311-win_amd64.whl", hash = "sha256:eef6ed6c03717083bc6d69c2d7ee8624205c29a8e6ea5a31cd3492ecdbaee1e1", size = 8065492 }, - { url = "https://files.pythonhosted.org/packages/eb/43/6b80eb47d1071f234ef0c96ca370c2ca621f91c12045f1401b5c9b28a639/matplotlib-3.10.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0ab1affc11d1f495ab9e6362b8174a25afc19c081ba5b0775ef00533a4236eea", size = 8179689 }, - { url = "https://files.pythonhosted.org/packages/0f/70/d61a591958325c357204870b5e7b164f93f2a8cca1dc6ce940f563909a13/matplotlib-3.10.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2a818d8bdcafa7ed2eed74487fdb071c09c1ae24152d403952adad11fa3c65b4", size = 8050466 }, - { url = "https://files.pythonhosted.org/packages/e7/75/70c9d2306203148cc7902a961240c5927dd8728afedf35e6a77e105a2985/matplotlib-3.10.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:748ebc3470c253e770b17d8b0557f0aa85cf8c63fd52f1a61af5b27ec0b7ffee", size = 8456252 }, - { url = "https://files.pythonhosted.org/packages/c4/91/ba0ae1ff4b3f30972ad01cd4a8029e70a0ec3b8ea5be04764b128b66f763/matplotlib-3.10.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed70453fd99733293ace1aec568255bc51c6361cb0da94fa5ebf0649fdb2150a", size = 8601321 }, - { url = "https://files.pythonhosted.org/packages/d2/88/d636041eb54a84b889e11872d91f7cbf036b3b0e194a70fa064eb8b04f7a/matplotlib-3.10.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dbed9917b44070e55640bd13419de83b4c918e52d97561544814ba463811cbc7", size = 9406972 }, - { url = "https://files.pythonhosted.org/packages/b1/79/0d1c165eac44405a86478082e225fce87874f7198300bbebc55faaf6d28d/matplotlib-3.10.3-cp312-cp312-win_amd64.whl", hash = "sha256:cf37d8c6ef1a48829443e8ba5227b44236d7fcaf7647caa3178a4ff9f7a5be05", size = 8067954 }, - { url = "https://files.pythonhosted.org/packages/3b/c1/23cfb566a74c696a3b338d8955c549900d18fe2b898b6e94d682ca21e7c2/matplotlib-3.10.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9f2efccc8dcf2b86fc4ee849eea5dcaecedd0773b30f47980dc0cbeabf26ec84", size = 8180318 }, - { url = "https://files.pythonhosted.org/packages/6c/0c/02f1c3b66b30da9ee343c343acbb6251bef5b01d34fad732446eaadcd108/matplotlib-3.10.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3ddbba06a6c126e3301c3d272a99dcbe7f6c24c14024e80307ff03791a5f294e", size = 8051132 }, - { url = "https://files.pythonhosted.org/packages/b4/ab/8db1a5ac9b3a7352fb914133001dae889f9fcecb3146541be46bed41339c/matplotlib-3.10.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:748302b33ae9326995b238f606e9ed840bf5886ebafcb233775d946aa8107a15", size = 8457633 }, - { url = "https://files.pythonhosted.org/packages/f5/64/41c4367bcaecbc03ef0d2a3ecee58a7065d0a36ae1aa817fe573a2da66d4/matplotlib-3.10.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a80fcccbef63302c0efd78042ea3c2436104c5b1a4d3ae20f864593696364ac7", size = 8601031 }, - { url = "https://files.pythonhosted.org/packages/12/6f/6cc79e9e5ab89d13ed64da28898e40fe5b105a9ab9c98f83abd24e46d7d7/matplotlib-3.10.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:55e46cbfe1f8586adb34f7587c3e4f7dedc59d5226719faf6cb54fc24f2fd52d", size = 9406988 }, - { url = "https://files.pythonhosted.org/packages/b1/0f/eed564407bd4d935ffabf561ed31099ed609e19287409a27b6d336848653/matplotlib-3.10.3-cp313-cp313-win_amd64.whl", hash = "sha256:151d89cb8d33cb23345cd12490c76fd5d18a56581a16d950b48c6ff19bb2ab93", size = 8068034 }, - { url = "https://files.pythonhosted.org/packages/3e/e5/2f14791ff69b12b09e9975e1d116d9578ac684460860ce542c2588cb7a1c/matplotlib-3.10.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:c26dd9834e74d164d06433dc7be5d75a1e9890b926b3e57e74fa446e1a62c3e2", size = 8218223 }, - { url = "https://files.pythonhosted.org/packages/5c/08/30a94afd828b6e02d0a52cae4a29d6e9ccfcf4c8b56cc28b021d3588873e/matplotlib-3.10.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:24853dad5b8c84c8c2390fc31ce4858b6df504156893292ce8092d190ef8151d", size = 8094985 }, - { url = "https://files.pythonhosted.org/packages/89/44/f3bc6b53066c889d7a1a3ea8094c13af6a667c5ca6220ec60ecceec2dabe/matplotlib-3.10.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68f7878214d369d7d4215e2a9075fef743be38fa401d32e6020bab2dfabaa566", size = 8483109 }, - { url = "https://files.pythonhosted.org/packages/ba/c7/473bc559beec08ebee9f86ca77a844b65747e1a6c2691e8c92e40b9f42a8/matplotlib-3.10.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6929fc618cb6db9cb75086f73b3219bbb25920cb24cee2ea7a12b04971a4158", size = 8618082 }, - { url = "https://files.pythonhosted.org/packages/d8/e9/6ce8edd264c8819e37bbed8172e0ccdc7107fe86999b76ab5752276357a4/matplotlib-3.10.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6c7818292a5cc372a2dc4c795e5c356942eb8350b98ef913f7fda51fe175ac5d", size = 9413699 }, - { url = "https://files.pythonhosted.org/packages/1b/92/9a45c91089c3cf690b5badd4be81e392ff086ccca8a1d4e3a08463d8a966/matplotlib-3.10.3-cp313-cp313t-win_amd64.whl", hash = "sha256:4f23ffe95c5667ef8a2b56eea9b53db7f43910fa4a2d5472ae0f72b64deab4d5", size = 8139044 }, - { url = "https://files.pythonhosted.org/packages/3d/d1/f54d43e95384b312ffa4a74a4326c722f3b8187aaaa12e9a84cdf3037131/matplotlib-3.10.3-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:86ab63d66bbc83fdb6733471d3bff40897c1e9921cba112accd748eee4bce5e4", size = 8162896 }, - { url = "https://files.pythonhosted.org/packages/24/a4/fbfc00c2346177c95b353dcf9b5a004106abe8730a62cb6f27e79df0a698/matplotlib-3.10.3-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:a48f9c08bf7444b5d2391a83e75edb464ccda3c380384b36532a0962593a1751", size = 8039702 }, - { url = "https://files.pythonhosted.org/packages/6a/b9/59e120d24a2ec5fc2d30646adb2efb4621aab3c6d83d66fb2a7a182db032/matplotlib-3.10.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb73d8aa75a237457988f9765e4dfe1c0d2453c5ca4eabc897d4309672c8e014", size = 8594298 }, -] - [[package]] name = "maturin" version = "1.9.1" @@ -1210,15 +675,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/37/48/ac2a9584402fb6c0cd5b5d1a91dcf176b15760130dd386bbafdbfe3640bf/numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00", size = 12812666 }, ] -[[package]] -name = "packaging" -version = "25.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469 }, -] - [[package]] name = "pathos" version = "0.3.4" @@ -1243,119 +699,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191 }, ] -[[package]] -name = "pillow" -version = "11.3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/d0d6dea55cd152ce3d6767bb38a8fc10e33796ba4ba210cbab9354b6d238/pillow-11.3.0.tar.gz", hash = "sha256:3828ee7586cd0b2091b6209e5ad53e20d0649bbe87164a459d0676e035e8f523", size = 47113069 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4c/5d/45a3553a253ac8763f3561371432a90bdbe6000fbdcf1397ffe502aa206c/pillow-11.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1b9c17fd4ace828b3003dfd1e30bff24863e0eb59b535e8f80194d9cc7ecf860", size = 5316554 }, - { url = "https://files.pythonhosted.org/packages/7c/c8/67c12ab069ef586a25a4a79ced553586748fad100c77c0ce59bb4983ac98/pillow-11.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:65dc69160114cdd0ca0f35cb434633c75e8e7fad4cf855177a05bf38678f73ad", size = 4686548 }, - { url = "https://files.pythonhosted.org/packages/2f/bd/6741ebd56263390b382ae4c5de02979af7f8bd9807346d068700dd6d5cf9/pillow-11.3.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7107195ddc914f656c7fc8e4a5e1c25f32e9236ea3ea860f257b0436011fddd0", size = 5859742 }, - { url = "https://files.pythonhosted.org/packages/ca/0b/c412a9e27e1e6a829e6ab6c2dca52dd563efbedf4c9c6aa453d9a9b77359/pillow-11.3.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc3e831b563b3114baac7ec2ee86819eb03caa1a2cef0b481a5675b59c4fe23b", size = 7633087 }, - { url = "https://files.pythonhosted.org/packages/59/9d/9b7076aaf30f5dd17e5e5589b2d2f5a5d7e30ff67a171eb686e4eecc2adf/pillow-11.3.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f1f182ebd2303acf8c380a54f615ec883322593320a9b00438eb842c1f37ae50", size = 5963350 }, - { url = "https://files.pythonhosted.org/packages/f0/16/1a6bf01fb622fb9cf5c91683823f073f053005c849b1f52ed613afcf8dae/pillow-11.3.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4445fa62e15936a028672fd48c4c11a66d641d2c05726c7ec1f8ba6a572036ae", size = 6631840 }, - { url = "https://files.pythonhosted.org/packages/7b/e6/6ff7077077eb47fde78739e7d570bdcd7c10495666b6afcd23ab56b19a43/pillow-11.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:71f511f6b3b91dd543282477be45a033e4845a40278fa8dcdbfdb07109bf18f9", size = 6074005 }, - { url = "https://files.pythonhosted.org/packages/c3/3a/b13f36832ea6d279a697231658199e0a03cd87ef12048016bdcc84131601/pillow-11.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:040a5b691b0713e1f6cbe222e0f4f74cd233421e105850ae3b3c0ceda520f42e", size = 6708372 }, - { url = "https://files.pythonhosted.org/packages/6c/e4/61b2e1a7528740efbc70b3d581f33937e38e98ef3d50b05007267a55bcb2/pillow-11.3.0-cp310-cp310-win32.whl", hash = "sha256:89bd777bc6624fe4115e9fac3352c79ed60f3bb18651420635f26e643e3dd1f6", size = 6277090 }, - { url = "https://files.pythonhosted.org/packages/a9/d3/60c781c83a785d6afbd6a326ed4d759d141de43aa7365725cbcd65ce5e54/pillow-11.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:19d2ff547c75b8e3ff46f4d9ef969a06c30ab2d4263a9e287733aa8b2429ce8f", size = 6985988 }, - { url = "https://files.pythonhosted.org/packages/9f/28/4f4a0203165eefb3763939c6789ba31013a2e90adffb456610f30f613850/pillow-11.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:819931d25e57b513242859ce1876c58c59dc31587847bf74cfe06b2e0cb22d2f", size = 2422899 }, - { url = "https://files.pythonhosted.org/packages/db/26/77f8ed17ca4ffd60e1dcd220a6ec6d71210ba398cfa33a13a1cd614c5613/pillow-11.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:1cd110edf822773368b396281a2293aeb91c90a2db00d78ea43e7e861631b722", size = 5316531 }, - { url = "https://files.pythonhosted.org/packages/cb/39/ee475903197ce709322a17a866892efb560f57900d9af2e55f86db51b0a5/pillow-11.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9c412fddd1b77a75aa904615ebaa6001f169b26fd467b4be93aded278266b288", size = 4686560 }, - { url = "https://files.pythonhosted.org/packages/d5/90/442068a160fd179938ba55ec8c97050a612426fae5ec0a764e345839f76d/pillow-11.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1aa4de119a0ecac0a34a9c8bde33f34022e2e8f99104e47a3ca392fd60e37d", size = 5870978 }, - { url = "https://files.pythonhosted.org/packages/13/92/dcdd147ab02daf405387f0218dcf792dc6dd5b14d2573d40b4caeef01059/pillow-11.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:91da1d88226663594e3f6b4b8c3c8d85bd504117d043740a8e0ec449087cc494", size = 7641168 }, - { url = "https://files.pythonhosted.org/packages/6e/db/839d6ba7fd38b51af641aa904e2960e7a5644d60ec754c046b7d2aee00e5/pillow-11.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:643f189248837533073c405ec2f0bb250ba54598cf80e8c1e043381a60632f58", size = 5973053 }, - { url = "https://files.pythonhosted.org/packages/f2/2f/d7675ecae6c43e9f12aa8d58b6012683b20b6edfbdac7abcb4e6af7a3784/pillow-11.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:106064daa23a745510dabce1d84f29137a37224831d88eb4ce94bb187b1d7e5f", size = 6640273 }, - { url = "https://files.pythonhosted.org/packages/45/ad/931694675ede172e15b2ff03c8144a0ddaea1d87adb72bb07655eaffb654/pillow-11.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cd8ff254faf15591e724dc7c4ddb6bf4793efcbe13802a4ae3e863cd300b493e", size = 6082043 }, - { url = "https://files.pythonhosted.org/packages/3a/04/ba8f2b11fc80d2dd462d7abec16351b45ec99cbbaea4387648a44190351a/pillow-11.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:932c754c2d51ad2b2271fd01c3d121daaa35e27efae2a616f77bf164bc0b3e94", size = 6715516 }, - { url = "https://files.pythonhosted.org/packages/48/59/8cd06d7f3944cc7d892e8533c56b0acb68399f640786313275faec1e3b6f/pillow-11.3.0-cp311-cp311-win32.whl", hash = "sha256:b4b8f3efc8d530a1544e5962bd6b403d5f7fe8b9e08227c6b255f98ad82b4ba0", size = 6274768 }, - { url = "https://files.pythonhosted.org/packages/f1/cc/29c0f5d64ab8eae20f3232da8f8571660aa0ab4b8f1331da5c2f5f9a938e/pillow-11.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:1a992e86b0dd7aeb1f053cd506508c0999d710a8f07b4c791c63843fc6a807ac", size = 6986055 }, - { url = "https://files.pythonhosted.org/packages/c6/df/90bd886fabd544c25addd63e5ca6932c86f2b701d5da6c7839387a076b4a/pillow-11.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:30807c931ff7c095620fe04448e2c2fc673fcbb1ffe2a7da3fb39613489b1ddd", size = 2423079 }, - { url = "https://files.pythonhosted.org/packages/40/fe/1bc9b3ee13f68487a99ac9529968035cca2f0a51ec36892060edcc51d06a/pillow-11.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdae223722da47b024b867c1ea0be64e0df702c5e0a60e27daad39bf960dd1e4", size = 5278800 }, - { url = "https://files.pythonhosted.org/packages/2c/32/7e2ac19b5713657384cec55f89065fb306b06af008cfd87e572035b27119/pillow-11.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:921bd305b10e82b4d1f5e802b6850677f965d8394203d182f078873851dada69", size = 4686296 }, - { url = "https://files.pythonhosted.org/packages/8e/1e/b9e12bbe6e4c2220effebc09ea0923a07a6da1e1f1bfbc8d7d29a01ce32b/pillow-11.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb76541cba2f958032d79d143b98a3a6b3ea87f0959bbe256c0b5e416599fd5d", size = 5871726 }, - { url = "https://files.pythonhosted.org/packages/8d/33/e9200d2bd7ba00dc3ddb78df1198a6e80d7669cce6c2bdbeb2530a74ec58/pillow-11.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:67172f2944ebba3d4a7b54f2e95c786a3a50c21b88456329314caaa28cda70f6", size = 7644652 }, - { url = "https://files.pythonhosted.org/packages/41/f1/6f2427a26fc683e00d985bc391bdd76d8dd4e92fac33d841127eb8fb2313/pillow-11.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f07ed9f56a3b9b5f49d3661dc9607484e85c67e27f3e8be2c7d28ca032fec7", size = 5977787 }, - { url = "https://files.pythonhosted.org/packages/e4/c9/06dd4a38974e24f932ff5f98ea3c546ce3f8c995d3f0985f8e5ba48bba19/pillow-11.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:676b2815362456b5b3216b4fd5bd89d362100dc6f4945154ff172e206a22c024", size = 6645236 }, - { url = "https://files.pythonhosted.org/packages/40/e7/848f69fb79843b3d91241bad658e9c14f39a32f71a301bcd1d139416d1be/pillow-11.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3e184b2f26ff146363dd07bde8b711833d7b0202e27d13540bfe2e35a323a809", size = 6086950 }, - { url = "https://files.pythonhosted.org/packages/0b/1a/7cff92e695a2a29ac1958c2a0fe4c0b2393b60aac13b04a4fe2735cad52d/pillow-11.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6be31e3fc9a621e071bc17bb7de63b85cbe0bfae91bb0363c893cbe67247780d", size = 6723358 }, - { url = "https://files.pythonhosted.org/packages/26/7d/73699ad77895f69edff76b0f332acc3d497f22f5d75e5360f78cbcaff248/pillow-11.3.0-cp312-cp312-win32.whl", hash = "sha256:7b161756381f0918e05e7cb8a371fff367e807770f8fe92ecb20d905d0e1c149", size = 6275079 }, - { url = "https://files.pythonhosted.org/packages/8c/ce/e7dfc873bdd9828f3b6e5c2bbb74e47a98ec23cc5c74fc4e54462f0d9204/pillow-11.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a6444696fce635783440b7f7a9fc24b3ad10a9ea3f0ab66c5905be1c19ccf17d", size = 6986324 }, - { url = "https://files.pythonhosted.org/packages/16/8f/b13447d1bf0b1f7467ce7d86f6e6edf66c0ad7cf44cf5c87a37f9bed9936/pillow-11.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:2aceea54f957dd4448264f9bf40875da0415c83eb85f55069d89c0ed436e3542", size = 2423067 }, - { url = "https://files.pythonhosted.org/packages/1e/93/0952f2ed8db3a5a4c7a11f91965d6184ebc8cd7cbb7941a260d5f018cd2d/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:1c627742b539bba4309df89171356fcb3cc5a9178355b2727d1b74a6cf155fbd", size = 2128328 }, - { url = "https://files.pythonhosted.org/packages/4b/e8/100c3d114b1a0bf4042f27e0f87d2f25e857e838034e98ca98fe7b8c0a9c/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:30b7c02f3899d10f13d7a48163c8969e4e653f8b43416d23d13d1bbfdc93b9f8", size = 2170652 }, - { url = "https://files.pythonhosted.org/packages/aa/86/3f758a28a6e381758545f7cdb4942e1cb79abd271bea932998fc0db93cb6/pillow-11.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7859a4cc7c9295f5838015d8cc0a9c215b77e43d07a25e460f35cf516df8626f", size = 2227443 }, - { url = "https://files.pythonhosted.org/packages/01/f4/91d5b3ffa718df2f53b0dc109877993e511f4fd055d7e9508682e8aba092/pillow-11.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec1ee50470b0d050984394423d96325b744d55c701a439d2bd66089bff963d3c", size = 5278474 }, - { url = "https://files.pythonhosted.org/packages/f9/0e/37d7d3eca6c879fbd9dba21268427dffda1ab00d4eb05b32923d4fbe3b12/pillow-11.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7db51d222548ccfd274e4572fdbf3e810a5e66b00608862f947b163e613b67dd", size = 4686038 }, - { url = "https://files.pythonhosted.org/packages/ff/b0/3426e5c7f6565e752d81221af9d3676fdbb4f352317ceafd42899aaf5d8a/pillow-11.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2d6fcc902a24ac74495df63faad1884282239265c6839a0a6416d33faedfae7e", size = 5864407 }, - { url = "https://files.pythonhosted.org/packages/fc/c1/c6c423134229f2a221ee53f838d4be9d82bab86f7e2f8e75e47b6bf6cd77/pillow-11.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f0f5d8f4a08090c6d6d578351a2b91acf519a54986c055af27e7a93feae6d3f1", size = 7639094 }, - { url = "https://files.pythonhosted.org/packages/ba/c9/09e6746630fe6372c67c648ff9deae52a2bc20897d51fa293571977ceb5d/pillow-11.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c37d8ba9411d6003bba9e518db0db0c58a680ab9fe5179f040b0463644bc9805", size = 5973503 }, - { url = "https://files.pythonhosted.org/packages/d5/1c/a2a29649c0b1983d3ef57ee87a66487fdeb45132df66ab30dd37f7dbe162/pillow-11.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13f87d581e71d9189ab21fe0efb5a23e9f28552d5be6979e84001d3b8505abe8", size = 6642574 }, - { url = "https://files.pythonhosted.org/packages/36/de/d5cc31cc4b055b6c6fd990e3e7f0f8aaf36229a2698501bcb0cdf67c7146/pillow-11.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:023f6d2d11784a465f09fd09a34b150ea4672e85fb3d05931d89f373ab14abb2", size = 6084060 }, - { url = "https://files.pythonhosted.org/packages/d5/ea/502d938cbaeec836ac28a9b730193716f0114c41325db428e6b280513f09/pillow-11.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:45dfc51ac5975b938e9809451c51734124e73b04d0f0ac621649821a63852e7b", size = 6721407 }, - { url = "https://files.pythonhosted.org/packages/45/9c/9c5e2a73f125f6cbc59cc7087c8f2d649a7ae453f83bd0362ff7c9e2aee2/pillow-11.3.0-cp313-cp313-win32.whl", hash = "sha256:a4d336baed65d50d37b88ca5b60c0fa9d81e3a87d4a7930d3880d1624d5b31f3", size = 6273841 }, - { url = "https://files.pythonhosted.org/packages/23/85/397c73524e0cd212067e0c969aa245b01d50183439550d24d9f55781b776/pillow-11.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0bce5c4fd0921f99d2e858dc4d4d64193407e1b99478bc5cacecba2311abde51", size = 6978450 }, - { url = "https://files.pythonhosted.org/packages/17/d2/622f4547f69cd173955194b78e4d19ca4935a1b0f03a302d655c9f6aae65/pillow-11.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:1904e1264881f682f02b7f8167935cce37bc97db457f8e7849dc3a6a52b99580", size = 2423055 }, - { url = "https://files.pythonhosted.org/packages/dd/80/a8a2ac21dda2e82480852978416cfacd439a4b490a501a288ecf4fe2532d/pillow-11.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4c834a3921375c48ee6b9624061076bc0a32a60b5532b322cc0ea64e639dd50e", size = 5281110 }, - { url = "https://files.pythonhosted.org/packages/44/d6/b79754ca790f315918732e18f82a8146d33bcd7f4494380457ea89eb883d/pillow-11.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5e05688ccef30ea69b9317a9ead994b93975104a677a36a8ed8106be9260aa6d", size = 4689547 }, - { url = "https://files.pythonhosted.org/packages/49/20/716b8717d331150cb00f7fdd78169c01e8e0c219732a78b0e59b6bdb2fd6/pillow-11.3.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1019b04af07fc0163e2810167918cb5add8d74674b6267616021ab558dc98ced", size = 5901554 }, - { url = "https://files.pythonhosted.org/packages/74/cf/a9f3a2514a65bb071075063a96f0a5cf949c2f2fce683c15ccc83b1c1cab/pillow-11.3.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f944255db153ebb2b19c51fe85dd99ef0ce494123f21b9db4877ffdfc5590c7c", size = 7669132 }, - { url = "https://files.pythonhosted.org/packages/98/3c/da78805cbdbee9cb43efe8261dd7cc0b4b93f2ac79b676c03159e9db2187/pillow-11.3.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f85acb69adf2aaee8b7da124efebbdb959a104db34d3a2cb0f3793dbae422a8", size = 6005001 }, - { url = "https://files.pythonhosted.org/packages/6c/fa/ce044b91faecf30e635321351bba32bab5a7e034c60187fe9698191aef4f/pillow-11.3.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05f6ecbeff5005399bb48d198f098a9b4b6bdf27b8487c7f38ca16eeb070cd59", size = 6668814 }, - { url = "https://files.pythonhosted.org/packages/7b/51/90f9291406d09bf93686434f9183aba27b831c10c87746ff49f127ee80cb/pillow-11.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a7bc6e6fd0395bc052f16b1a8670859964dbd7003bd0af2ff08342eb6e442cfe", size = 6113124 }, - { url = "https://files.pythonhosted.org/packages/cd/5a/6fec59b1dfb619234f7636d4157d11fb4e196caeee220232a8d2ec48488d/pillow-11.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:83e1b0161c9d148125083a35c1c5a89db5b7054834fd4387499e06552035236c", size = 6747186 }, - { url = "https://files.pythonhosted.org/packages/49/6b/00187a044f98255225f172de653941e61da37104a9ea60e4f6887717e2b5/pillow-11.3.0-cp313-cp313t-win32.whl", hash = "sha256:2a3117c06b8fb646639dce83694f2f9eac405472713fcb1ae887469c0d4f6788", size = 6277546 }, - { url = "https://files.pythonhosted.org/packages/e8/5c/6caaba7e261c0d75bab23be79f1d06b5ad2a2ae49f028ccec801b0e853d6/pillow-11.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:857844335c95bea93fb39e0fa2726b4d9d758850b34075a7e3ff4f4fa3aa3b31", size = 6985102 }, - { url = "https://files.pythonhosted.org/packages/f3/7e/b623008460c09a0cb38263c93b828c666493caee2eb34ff67f778b87e58c/pillow-11.3.0-cp313-cp313t-win_arm64.whl", hash = "sha256:8797edc41f3e8536ae4b10897ee2f637235c94f27404cac7297f7b607dd0716e", size = 2424803 }, - { url = "https://files.pythonhosted.org/packages/73/f4/04905af42837292ed86cb1b1dabe03dce1edc008ef14c473c5c7e1443c5d/pillow-11.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d9da3df5f9ea2a89b81bb6087177fb1f4d1c7146d583a3fe5c672c0d94e55e12", size = 5278520 }, - { url = "https://files.pythonhosted.org/packages/41/b0/33d79e377a336247df6348a54e6d2a2b85d644ca202555e3faa0cf811ecc/pillow-11.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0b275ff9b04df7b640c59ec5a3cb113eefd3795a8df80bac69646ef699c6981a", size = 4686116 }, - { url = "https://files.pythonhosted.org/packages/49/2d/ed8bc0ab219ae8768f529597d9509d184fe8a6c4741a6864fea334d25f3f/pillow-11.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0743841cabd3dba6a83f38a92672cccbd69af56e3e91777b0ee7f4dba4385632", size = 5864597 }, - { url = "https://files.pythonhosted.org/packages/b5/3d/b932bb4225c80b58dfadaca9d42d08d0b7064d2d1791b6a237f87f661834/pillow-11.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2465a69cf967b8b49ee1b96d76718cd98c4e925414ead59fdf75cf0fd07df673", size = 7638246 }, - { url = "https://files.pythonhosted.org/packages/09/b5/0487044b7c096f1b48f0d7ad416472c02e0e4bf6919541b111efd3cae690/pillow-11.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41742638139424703b4d01665b807c6468e23e699e8e90cffefe291c5832b027", size = 5973336 }, - { url = "https://files.pythonhosted.org/packages/a8/2d/524f9318f6cbfcc79fbc004801ea6b607ec3f843977652fdee4857a7568b/pillow-11.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93efb0b4de7e340d99057415c749175e24c8864302369e05914682ba642e5d77", size = 6642699 }, - { url = "https://files.pythonhosted.org/packages/6f/d2/a9a4f280c6aefedce1e8f615baaa5474e0701d86dd6f1dede66726462bbd/pillow-11.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7966e38dcd0fa11ca390aed7c6f20454443581d758242023cf36fcb319b1a874", size = 6083789 }, - { url = "https://files.pythonhosted.org/packages/fe/54/86b0cd9dbb683a9d5e960b66c7379e821a19be4ac5810e2e5a715c09a0c0/pillow-11.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:98a9afa7b9007c67ed84c57c9e0ad86a6000da96eaa638e4f8abe5b65ff83f0a", size = 6720386 }, - { url = "https://files.pythonhosted.org/packages/e7/95/88efcaf384c3588e24259c4203b909cbe3e3c2d887af9e938c2022c9dd48/pillow-11.3.0-cp314-cp314-win32.whl", hash = "sha256:02a723e6bf909e7cea0dac1b0e0310be9d7650cd66222a5f1c571455c0a45214", size = 6370911 }, - { url = "https://files.pythonhosted.org/packages/2e/cc/934e5820850ec5eb107e7b1a72dd278140731c669f396110ebc326f2a503/pillow-11.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:a418486160228f64dd9e9efcd132679b7a02a5f22c982c78b6fc7dab3fefb635", size = 7117383 }, - { url = "https://files.pythonhosted.org/packages/d6/e9/9c0a616a71da2a5d163aa37405e8aced9a906d574b4a214bede134e731bc/pillow-11.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:155658efb5e044669c08896c0c44231c5e9abcaadbc5cd3648df2f7c0b96b9a6", size = 2511385 }, - { url = "https://files.pythonhosted.org/packages/1a/33/c88376898aff369658b225262cd4f2659b13e8178e7534df9e6e1fa289f6/pillow-11.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:59a03cdf019efbfeeed910bf79c7c93255c3d54bc45898ac2a4140071b02b4ae", size = 5281129 }, - { url = "https://files.pythonhosted.org/packages/1f/70/d376247fb36f1844b42910911c83a02d5544ebd2a8bad9efcc0f707ea774/pillow-11.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f8a5827f84d973d8636e9dc5764af4f0cf2318d26744b3d902931701b0d46653", size = 4689580 }, - { url = "https://files.pythonhosted.org/packages/eb/1c/537e930496149fbac69efd2fc4329035bbe2e5475b4165439e3be9cb183b/pillow-11.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ee92f2fd10f4adc4b43d07ec5e779932b4eb3dbfbc34790ada5a6669bc095aa6", size = 5902860 }, - { url = "https://files.pythonhosted.org/packages/bd/57/80f53264954dcefeebcf9dae6e3eb1daea1b488f0be8b8fef12f79a3eb10/pillow-11.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c96d333dcf42d01f47b37e0979b6bd73ec91eae18614864622d9b87bbd5bbf36", size = 7670694 }, - { url = "https://files.pythonhosted.org/packages/70/ff/4727d3b71a8578b4587d9c276e90efad2d6fe0335fd76742a6da08132e8c/pillow-11.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c96f993ab8c98460cd0c001447bff6194403e8b1d7e149ade5f00594918128b", size = 6005888 }, - { url = "https://files.pythonhosted.org/packages/05/ae/716592277934f85d3be51d7256f3636672d7b1abfafdc42cf3f8cbd4b4c8/pillow-11.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41342b64afeba938edb034d122b2dda5db2139b9a4af999729ba8818e0056477", size = 6670330 }, - { url = "https://files.pythonhosted.org/packages/e7/bb/7fe6cddcc8827b01b1a9766f5fdeb7418680744f9082035bdbabecf1d57f/pillow-11.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:068d9c39a2d1b358eb9f245ce7ab1b5c3246c7c8c7d9ba58cfa5b43146c06e50", size = 6114089 }, - { url = "https://files.pythonhosted.org/packages/8b/f5/06bfaa444c8e80f1a8e4bff98da9c83b37b5be3b1deaa43d27a0db37ef84/pillow-11.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a1bc6ba083b145187f648b667e05a2534ecc4b9f2784c2cbe3089e44868f2b9b", size = 6748206 }, - { url = "https://files.pythonhosted.org/packages/f0/77/bc6f92a3e8e6e46c0ca78abfffec0037845800ea38c73483760362804c41/pillow-11.3.0-cp314-cp314t-win32.whl", hash = "sha256:118ca10c0d60b06d006be10a501fd6bbdfef559251ed31b794668ed569c87e12", size = 6377370 }, - { url = "https://files.pythonhosted.org/packages/4a/82/3a721f7d69dca802befb8af08b7c79ebcab461007ce1c18bd91a5d5896f9/pillow-11.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8924748b688aa210d79883357d102cd64690e56b923a186f35a82cbc10f997db", size = 7121500 }, - { url = "https://files.pythonhosted.org/packages/89/c7/5572fa4a3f45740eaab6ae86fcdf7195b55beac1371ac8c619d880cfe948/pillow-11.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:79ea0d14d3ebad43ec77ad5272e6ff9bba5b679ef73375ea760261207fa8e0aa", size = 2512835 }, - { url = "https://files.pythonhosted.org/packages/9e/8e/9c089f01677d1264ab8648352dcb7773f37da6ad002542760c80107da816/pillow-11.3.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:48d254f8a4c776de343051023eb61ffe818299eeac478da55227d96e241de53f", size = 5316478 }, - { url = "https://files.pythonhosted.org/packages/b5/a9/5749930caf674695867eb56a581e78eb5f524b7583ff10b01b6e5048acb3/pillow-11.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7aee118e30a4cf54fdd873bd3a29de51e29105ab11f9aad8c32123f58c8f8081", size = 4686522 }, - { url = "https://files.pythonhosted.org/packages/43/46/0b85b763eb292b691030795f9f6bb6fcaf8948c39413c81696a01c3577f7/pillow-11.3.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:23cff760a9049c502721bdb743a7cb3e03365fafcdfc2ef9784610714166e5a4", size = 5853376 }, - { url = "https://files.pythonhosted.org/packages/5e/c6/1a230ec0067243cbd60bc2dad5dc3ab46a8a41e21c15f5c9b52b26873069/pillow-11.3.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6359a3bc43f57d5b375d1ad54a0074318a0844d11b76abccf478c37c986d3cfc", size = 7626020 }, - { url = "https://files.pythonhosted.org/packages/63/dd/f296c27ffba447bfad76c6a0c44c1ea97a90cb9472b9304c94a732e8dbfb/pillow-11.3.0-cp39-cp39-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:092c80c76635f5ecb10f3f83d76716165c96f5229addbd1ec2bdbbda7d496e06", size = 5956732 }, - { url = "https://files.pythonhosted.org/packages/a5/a0/98a3630f0b57f77bae67716562513d3032ae70414fcaf02750279c389a9e/pillow-11.3.0-cp39-cp39-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cadc9e0ea0a2431124cde7e1697106471fc4c1da01530e679b2391c37d3fbb3a", size = 6624404 }, - { url = "https://files.pythonhosted.org/packages/de/e6/83dfba5646a290edd9a21964da07674409e410579c341fc5b8f7abd81620/pillow-11.3.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:6a418691000f2a418c9135a7cf0d797c1bb7d9a485e61fe8e7722845b95ef978", size = 6067760 }, - { url = "https://files.pythonhosted.org/packages/bc/41/15ab268fe6ee9a2bc7391e2bbb20a98d3974304ab1a406a992dcb297a370/pillow-11.3.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:97afb3a00b65cc0804d1c7abddbf090a81eaac02768af58cbdcaaa0a931e0b6d", size = 6700534 }, - { url = "https://files.pythonhosted.org/packages/64/79/6d4f638b288300bed727ff29f2a3cb63db054b33518a95f27724915e3fbc/pillow-11.3.0-cp39-cp39-win32.whl", hash = "sha256:ea944117a7974ae78059fcc1800e5d3295172bb97035c0c1d9345fca1419da71", size = 6277091 }, - { url = "https://files.pythonhosted.org/packages/46/05/4106422f45a05716fd34ed21763f8ec182e8ea00af6e9cb05b93a247361a/pillow-11.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:e5c5858ad8ec655450a7c7df532e9842cf8df7cc349df7225c60d5d348c8aada", size = 6986091 }, - { url = "https://files.pythonhosted.org/packages/63/c6/287fd55c2c12761d0591549d48885187579b7c257bef0c6660755b0b59ae/pillow-11.3.0-cp39-cp39-win_arm64.whl", hash = "sha256:6abdbfd3aea42be05702a8dd98832329c167ee84400a1d1f61ab11437f1717eb", size = 2422632 }, - { url = "https://files.pythonhosted.org/packages/6f/8b/209bd6b62ce8367f47e68a218bffac88888fdf2c9fcf1ecadc6c3ec1ebc7/pillow-11.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3cee80663f29e3843b68199b9d6f4f54bd1d4a6b59bdd91bceefc51238bcb967", size = 5270556 }, - { url = "https://files.pythonhosted.org/packages/2e/e6/231a0b76070c2cfd9e260a7a5b504fb72da0a95279410fa7afd99d9751d6/pillow-11.3.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b5f56c3f344f2ccaf0dd875d3e180f631dc60a51b314295a3e681fe8cf851fbe", size = 4654625 }, - { url = "https://files.pythonhosted.org/packages/13/f4/10cf94fda33cb12765f2397fc285fa6d8eb9c29de7f3185165b702fc7386/pillow-11.3.0-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e67d793d180c9df62f1f40aee3accca4829d3794c95098887edc18af4b8b780c", size = 4874207 }, - { url = "https://files.pythonhosted.org/packages/72/c9/583821097dc691880c92892e8e2d41fe0a5a3d6021f4963371d2f6d57250/pillow-11.3.0-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d000f46e2917c705e9fb93a3606ee4a819d1e3aa7a9b442f6444f07e77cf5e25", size = 6583939 }, - { url = "https://files.pythonhosted.org/packages/3b/8e/5c9d410f9217b12320efc7c413e72693f48468979a013ad17fd690397b9a/pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:527b37216b6ac3a12d7838dc3bd75208ec57c1c6d11ef01902266a5a0c14fc27", size = 4957166 }, - { url = "https://files.pythonhosted.org/packages/62/bb/78347dbe13219991877ffb3a91bf09da8317fbfcd4b5f9140aeae020ad71/pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:be5463ac478b623b9dd3937afd7fb7ab3d79dd290a28e2b6df292dc75063eb8a", size = 5581482 }, - { url = "https://files.pythonhosted.org/packages/d9/28/1000353d5e61498aaeaaf7f1e4b49ddb05f2c6575f9d4f9f914a3538b6e1/pillow-11.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:8dc70ca24c110503e16918a658b869019126ecfe03109b754c402daff12b3d9f", size = 6984596 }, - { url = "https://files.pythonhosted.org/packages/9e/e3/6fa84033758276fb31da12e5fb66ad747ae83b93c67af17f8c6ff4cc8f34/pillow-11.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7c8ec7a017ad1bd562f93dbd8505763e688d388cde6e4a010ae1486916e713e6", size = 5270566 }, - { url = "https://files.pythonhosted.org/packages/5b/ee/e8d2e1ab4892970b561e1ba96cbd59c0d28cf66737fc44abb2aec3795a4e/pillow-11.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9ab6ae226de48019caa8074894544af5b53a117ccb9d3b3dcb2871464c829438", size = 4654618 }, - { url = "https://files.pythonhosted.org/packages/f2/6d/17f80f4e1f0761f02160fc433abd4109fa1548dcfdca46cfdadaf9efa565/pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fe27fb049cdcca11f11a7bfda64043c37b30e6b91f10cb5bab275806c32f6ab3", size = 4874248 }, - { url = "https://files.pythonhosted.org/packages/de/5f/c22340acd61cef960130585bbe2120e2fd8434c214802f07e8c03596b17e/pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:465b9e8844e3c3519a983d58b80be3f668e2a7a5db97f2784e7079fbc9f9822c", size = 6583963 }, - { url = "https://files.pythonhosted.org/packages/31/5e/03966aedfbfcbb4d5f8aa042452d3361f325b963ebbadddac05b122e47dd/pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5418b53c0d59b3824d05e029669efa023bbef0f3e92e75ec8428f3799487f361", size = 4957170 }, - { url = "https://files.pythonhosted.org/packages/cc/2d/e082982aacc927fc2cab48e1e731bdb1643a1406acace8bed0900a61464e/pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:504b6f59505f08ae014f724b6207ff6222662aab5cc9542577fb084ed0676ac7", size = 5581505 }, - { url = "https://files.pythonhosted.org/packages/34/e7/ae39f538fd6844e982063c3a5e4598b8ced43b9633baa3a85ef33af8c05c/pillow-11.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c84d689db21a1c397d001aa08241044aa2069e7587b398c8cc63020390b1c1b8", size = 6984598 }, -] - [[package]] name = "polars" version = "1.31.0" @@ -1475,27 +818,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 }, ] -[[package]] -name = "pyparsing" -version = "3.2.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/bb/22/f1129e69d94ffff626bdb5c835506b3a5b4f3d070f17ea295e12c2c6f60f/pyparsing-3.2.3.tar.gz", hash = "sha256:b9c13f1ab8b3b542f72e28f634bad4de758ab3ce4546e4301970ad6fa77c38be", size = 1088608 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/05/e7/df2285f3d08fee213f2d041540fa4fc9ca6c2d44cf36d3a035bf2a8d2bcc/pyparsing-3.2.3-py3-none-any.whl", hash = "sha256:a749938e02d6fd0b59b356ca504a24982314bb090c383e3cf201c95ef7e2bfcf", size = 111120 }, -] - -[[package]] -name = "python-dateutil" -version = "2.9.0.post0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "six" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 }, -] - [[package]] name = "pyzmq" version = "27.0.0" @@ -1571,15 +893,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/51/8b/619a9ee2fa4d3c724fbadde946427735ade64da03894b071bbdc3b789d83/pyzmq-27.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:096af9e133fec3a72108ddefba1e42985cb3639e9de52cfd336b6fc23aa083e9", size = 544715 }, ] -[[package]] -name = "six" -version = "1.17.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 }, -] - [[package]] name = "tomli" version = "2.2.1" @@ -1636,12 +949,3 @@ sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09 wheels = [ { url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906 }, ] - -[[package]] -name = "zipp" -version = "3.23.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276 }, -]