From 4f2d0a3381ae5d12be12a2983a5fc59b640e1ac1 Mon Sep 17 00:00:00 2001 From: Lukas Kikuchi Date: Sat, 25 Apr 2026 15:31:18 +0100 Subject: [PATCH 01/15] fix(netrun): harden ExecutionManager job lifecycle, add observability Phase 0 of the major refactor. Fixes three confirmed bugs and adds two observability improvements, with regression tests for each. Bug fixes: - ExecutionManager.run() now cleans up _worker_jobs and _msgs via try/finally on cancellation/timeout (previously leaked stale entries, corrupting LEAST_BUSY allocation) - Sync and async worker functions now catch exceptions and send error UP_RUN_RESPONSE instead of crashing the worker loop or hanging the client forever - _msg_recv_task_func silently discards orphaned responses for cancelled jobs instead of raising KeyError - netrun validate now surfaces resolution errors as warnings instead of silently swallowing them Observability: - faulthandler + SIGUSR1 registered in Net.init() for stack dumps on hang - gc.collect() + peak RSS logging (DEBUG level) between retry attempts Co-Authored-By: Claude Opus 4.6 (1M context) --- netrun/pts/netrun/05_execution_manager.pct.py | 208 +++++++++++------- netrun/pts/netrun/06_net/01_net/02_net.pct.py | 27 +++ netrun/pts/netrun/10_cli/02_commands.pct.py | 14 +- .../test_execution_manager_async.pct.py | 48 ++++ .../test_execution_manager_thread.pct.py | 106 +++++++++ netrun/pts/tests/06_net/test_net.pct.py | 116 ++++++++++ netrun/pts/tests/10_cli/test_cli.pct.py | 48 ++++ netrun/src/netrun/cli/_commands.py | 14 +- netrun/src/netrun/execution_manager.py | 208 +++++++++++------- netrun/src/netrun/net/_net/_net.py | 27 +++ netrun/src/tests/cli/test_cli.py | 46 +++- .../test_execution_manager_async.py | 43 +++- .../test_execution_manager_thread.py | 94 +++++++- netrun/src/tests/net/test_net.py | 104 ++++++++- 14 files changed, 927 insertions(+), 176 deletions(-) diff --git a/netrun/pts/netrun/05_execution_manager.pct.py b/netrun/pts/netrun/05_execution_manager.pct.py index ca087641..467320f4 100644 --- a/netrun/pts/netrun/05_execution_manager.pct.py +++ b/netrun/pts/netrun/05_execution_manager.pct.py @@ -173,41 +173,56 @@ def _worker_func( # RUN if key == ExecutionManagerProtocolKeys.RUN.value: msg_id, func_import_path_or_key, run_id, send_channel, args, kwargs = data - if func_import_path_or_key in registered_functions: - func = registered_functions[func_import_path_or_key] - else: - module_path, func_name = func_import_path_or_key.rsplit(".", 1) - module = importlib.import_module(module_path) - func = getattr(module, func_name) - - if func_preprocessor is not None: - func = func_preprocessor(func) - - timestamp_utc_started = get_timestamp_utc() - channel.send(ExecutionManagerProtocolKeys.UP_RUN_STARTED.value, (msg_id, timestamp_utc_started)) - res = _func_runner( - channel=channel, - func=func, - send_channel=send_channel, - args=args, - kwargs=kwargs, - event_loop=event_loop, - ) - - # Call done callback if provided - if func_done_callback is not None: - if send_channel: - func_done_callback(channel, *args, **kwargs, result=res) + timestamp_utc_started = None + started_sent = False + try: + if func_import_path_or_key in registered_functions: + func = registered_functions[func_import_path_or_key] else: - func_done_callback(*args, **kwargs, result=res) - - timestamp_utc_completed = get_timestamp_utc() - if is_in_main_process: - converted_to_str, _res = False, res - else: - converted_to_str, _res = _convert_to_str_if_not_serializable(res) - - channel.send(ExecutionManagerProtocolKeys.UP_RUN_RESPONSE.value, (msg_id, timestamp_utc_started, timestamp_utc_completed, converted_to_str, _res)) + module_path, func_name = func_import_path_or_key.rsplit(".", 1) + module = importlib.import_module(module_path) + func = getattr(module, func_name) + + if func_preprocessor is not None: + func = func_preprocessor(func) + + timestamp_utc_started = get_timestamp_utc() + channel.send(ExecutionManagerProtocolKeys.UP_RUN_STARTED.value, (msg_id, timestamp_utc_started)) + started_sent = True + res = _func_runner( + channel=channel, + func=func, + send_channel=send_channel, + args=args, + kwargs=kwargs, + event_loop=event_loop, + ) + + # Call done callback if provided + if func_done_callback is not None: + if send_channel: + func_done_callback(channel, *args, **kwargs, result=res) + else: + func_done_callback(*args, **kwargs, result=res) + + timestamp_utc_completed = get_timestamp_utc() + if is_in_main_process: + converted_to_str, _res = False, res + else: + converted_to_str, _res = _convert_to_str_if_not_serializable(res) + + channel.send(ExecutionManagerProtocolKeys.UP_RUN_RESPONSE.value, (msg_id, timestamp_utc_started, timestamp_utc_completed, converted_to_str, _res)) + except Exception as e: + # Send error response so the client doesn't hang forever + timestamp_utc_error = get_timestamp_utc() + if timestamp_utc_started is None: + timestamp_utc_started = timestamp_utc_error + try: + if not started_sent: + channel.send(ExecutionManagerProtocolKeys.UP_RUN_STARTED.value, (msg_id, timestamp_utc_started)) + channel.send(ExecutionManagerProtocolKeys.UP_RUN_RESPONSE.value, (msg_id, timestamp_utc_started, timestamp_utc_error, False, e)) + except Exception: + pass # Worker loop must survive # SEND_FUNCTION elif key == ExecutionManagerProtocolKeys.SEND_FUNCTION.value: msg_id, func_key, func = data @@ -251,30 +266,38 @@ async def _async_worker_func( async def _handle_run(msg_id, func, send_channel, args, kwargs): """Execute a single RUN request and send the response.""" timestamp_utc_started = get_timestamp_utc() - await channel.send(ExecutionManagerProtocolKeys.UP_RUN_STARTED.value, (msg_id, timestamp_utc_started)) - - res = await _async_func_runner( - channel=channel, - func=func, - send_channel=send_channel, - args=args, - kwargs=kwargs, - ) - - # Call done callback if provided - if func_done_callback is not None: - if send_channel: - callback_result = func_done_callback(channel, *args, **kwargs, result=res) - else: - callback_result = func_done_callback(*args, **kwargs, result=res) - # Await if the callback is async - if asyncio.iscoroutine(callback_result): - await callback_result - - timestamp_utc_completed = get_timestamp_utc() - converted_to_str, _res = False, res + try: + await channel.send(ExecutionManagerProtocolKeys.UP_RUN_STARTED.value, (msg_id, timestamp_utc_started)) + + res = await _async_func_runner( + channel=channel, + func=func, + send_channel=send_channel, + args=args, + kwargs=kwargs, + ) - await channel.send(ExecutionManagerProtocolKeys.UP_RUN_RESPONSE.value, (msg_id, timestamp_utc_started, timestamp_utc_completed, converted_to_str, _res)) + # Call done callback if provided + if func_done_callback is not None: + if send_channel: + callback_result = func_done_callback(channel, *args, **kwargs, result=res) + else: + callback_result = func_done_callback(*args, **kwargs, result=res) + # Await if the callback is async + if asyncio.iscoroutine(callback_result): + await callback_result + + timestamp_utc_completed = get_timestamp_utc() + converted_to_str, _res = False, res + + await channel.send(ExecutionManagerProtocolKeys.UP_RUN_RESPONSE.value, (msg_id, timestamp_utc_started, timestamp_utc_completed, converted_to_str, _res)) + except Exception as e: + # Send error response so the client doesn't hang forever + timestamp_utc_error = get_timestamp_utc() + try: + await channel.send(ExecutionManagerProtocolKeys.UP_RUN_RESPONSE.value, (msg_id, timestamp_utc_started, timestamp_utc_error, False, e)) + except Exception: + pass # If we can't send the error, the client will eventually timeout pending_tasks: set[asyncio.Task] = set() @@ -511,7 +534,12 @@ async def _msg_recv_task_func(self, pool_id: str): msg = await pool.recv() msg_id = msg.data[0] msg.data = msg.data[1:] - await self._msgs[pool_id][msg_id].put(msg) + # Queue may have been cleaned up if the job was cancelled/timed out. + # Silently discard orphaned responses. + queue = self._msgs[pool_id].get(msg_id) + if queue is not None: + await queue.put(msg) + async def _send_msg(self, pool_id: str, worker_id: str, key: str, data: Any) -> str: pool = self._pools[pool_id] @@ -622,32 +650,46 @@ async def run( self._worker_round_robin_lst.remove((pool_id, worker_id)) self._worker_round_robin_lst.append((pool_id, worker_id)) - msg_id = await self._send_msg( - pool_id=pool_id, - worker_id=worker_id, - key=ExecutionManagerProtocolKeys.RUN.value, - data=(func_import_path_or_key, run_id, send_channel, func_args, func_kwargs), - ) + msg_id = None + try: + msg_id = await self._send_msg( + pool_id=pool_id, + worker_id=worker_id, + key=ExecutionManagerProtocolKeys.RUN.value, + data=(func_import_path_or_key, run_id, send_channel, func_args, func_kwargs), + ) - # Wait for UP_RUN_STARTED - started_msg = await self._recv_msg(pool_id, msg_id, expect=ExecutionManagerProtocolKeys.UP_RUN_STARTED, close_msg_queue=False) - job_info.timestamp_utc_started = started_msg.data[0] # timestamp_utc_started - - # Wait for UP_RUN_RESPONSE - msg = await self._recv_msg(pool_id, msg_id, expect=ExecutionManagerProtocolKeys.UP_RUN_RESPONSE, close_msg_queue=True) - self._worker_jobs[(pool_id, worker_id)].remove(job_info) - - timestamp_utc_started, timestamp_utc_completed, converted_to_str, _res = msg.data - return JobResult( - timestamp_utc_submitted=job_info.timestamp_utc_submitted, - timestamp_utc_started=job_info.timestamp_utc_started, - timestamp_utc_completed=timestamp_utc_completed, - func_import_path_or_key=job_info.func_import_path_or_key, - pool_id=job_info.pool_id, - worker_id=job_info.worker_id, - converted_to_str=converted_to_str, - result=_res, - ) + # Wait for UP_RUN_STARTED + started_msg = await self._recv_msg(pool_id, msg_id, expect=ExecutionManagerProtocolKeys.UP_RUN_STARTED, close_msg_queue=False) + job_info.timestamp_utc_started = started_msg.data[0] # timestamp_utc_started + + # Wait for UP_RUN_RESPONSE + msg = await self._recv_msg(pool_id, msg_id, expect=ExecutionManagerProtocolKeys.UP_RUN_RESPONSE, close_msg_queue=True) + msg_id = None # Already cleaned up by close_msg_queue=True + + self._worker_jobs[(pool_id, worker_id)].remove(job_info) + + timestamp_utc_started, timestamp_utc_completed, converted_to_str, _res = msg.data + if isinstance(_res, Exception): + raise _res + return JobResult( + timestamp_utc_submitted=job_info.timestamp_utc_submitted, + timestamp_utc_started=job_info.timestamp_utc_started, + timestamp_utc_completed=timestamp_utc_completed, + func_import_path_or_key=job_info.func_import_path_or_key, + pool_id=job_info.pool_id, + worker_id=job_info.worker_id, + converted_to_str=converted_to_str, + result=_res, + ) + finally: + # Clean up leaked state on cancellation/exception + try: + self._worker_jobs[(pool_id, worker_id)].remove(job_info) + except ValueError: + pass # Already removed on the success path + if msg_id is not None and msg_id in self._msgs.get(pool_id, {}): + del self._msgs[pool_id][msg_id] async def run_allocate( self, diff --git a/netrun/pts/netrun/06_net/01_net/02_net.pct.py b/netrun/pts/netrun/06_net/01_net/02_net.pct.py index f5afed99..59c176f8 100644 --- a/netrun/pts/netrun/06_net/01_net/02_net.pct.py +++ b/netrun/pts/netrun/06_net/01_net/02_net.pct.py @@ -1259,6 +1259,15 @@ async def init(self, run_init_nodes: bool | None = None) -> None: if self._initialized: raise RuntimeError("Net already initialized") + # Enable faulthandler for debugging (SIGUSR1 -> stack dump of all threads) + import faulthandler + try: + faulthandler.enable() + if hasattr(signal, 'SIGUSR1'): + faulthandler.register(signal.SIGUSR1, all_threads=True) + except (OSError, ValueError, IOError): + pass # stderr may not support fileno (mocked in tests), or signal unavailable + await self._execution_manager.start() # Register node functions with all workers @@ -2934,6 +2943,24 @@ async def _handle_epoch_failure( if effective_retry_wait > 0: await asyncio.sleep(effective_retry_wait) + # Force garbage collection to reclaim resources from the failed attempt + import gc + gc.collect() + + # Log peak RSS for debugging memory issues during retries + try: + import resource + import platform as _platform + import logging + rss_raw = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss + rss_mb = rss_raw / (1024 * 1024) if _platform.system() == "Darwin" else rss_raw / 1024 + logging.getLogger("netrun.net").debug( + "Retry %d/%d for node '%s' (epoch %s), peak RSS: %.1f MB", + retry_count + 1, effective_retries, node_name, epoch_id, rss_mb, + ) + except ImportError: + pass # resource module not available (Windows) + # Retry (deferred actions were discarded, so we start fresh) return await self._execute_epoch_with_retry( epoch_id=epoch_id, diff --git a/netrun/pts/netrun/10_cli/02_commands.pct.py b/netrun/pts/netrun/10_cli/02_commands.pct.py index 54572930..e6a78d59 100644 --- a/netrun/pts/netrun/10_cli/02_commands.pct.py +++ b/netrun/pts/netrun/10_cli/02_commands.pct.py @@ -54,6 +54,7 @@ def validate( """Validate a netrun config file.""" config_path = find_config(config) errors: list[str] = [] + warnings: list[str] = [] node_count = 0 edge_count = 0 @@ -82,8 +83,9 @@ def validate( # Step 3: Post-resolution validation via Rust (only if resolve succeeds) try: resolved = net_config.resolve() - except Exception: + except Exception as e: resolved = None + warnings.append(f"Resolution error: {e}") if resolved is not None: try: @@ -128,10 +130,16 @@ def _check_actions(actions: list[dict], prefix: str) -> None: errors.append(f"Raw data check error: {e}") if errors: - output_json({"valid": False, "file": str(config_path), "errors": errors}, pretty) + result = {"valid": False, "file": str(config_path), "errors": errors} + if warnings: + result["warnings"] = warnings + output_json(result, pretty) raise typer.Exit(1) else: - output_json({"valid": True, "file": str(config_path), "nodes": node_count, "edges": edge_count}, pretty) + result = {"valid": True, "file": str(config_path), "nodes": node_count, "edges": edge_count} + if warnings: + result["warnings"] = warnings + output_json(result, pretty) # %% [markdown] # ## structure diff --git a/netrun/pts/tests/05_execution_manager/test_execution_manager_async.pct.py b/netrun/pts/tests/05_execution_manager/test_execution_manager_async.pct.py index 174bd9d5..8858790e 100644 --- a/netrun/pts/tests/05_execution_manager/test_execution_manager_async.pct.py +++ b/netrun/pts/tests/05_execution_manager/test_execution_manager_async.pct.py @@ -32,6 +32,7 @@ add_numbers, multiply_numbers, slow_function, + function_with_error, function_returns_non_serializable, function_with_kwargs, async_add, @@ -552,3 +553,50 @@ async def slow_async_func(delay: float) -> str: # %% await test_concurrent_async_execution(); + +# %% [markdown] +# ## Regression: Async worker exception does not hang + +# %% +#|export +@pytest.mark.asyncio +async def test_worker_exception_does_not_hang(): + """Test that worker-level exceptions result in an error, not a hang. + + Regression test: async worker swallows exceptions in _handle_run, + causing run() to hang forever waiting for UP_RUN_RESPONSE. + """ + manager = ExecutionManager({ + "pool": (SingleWorkerPool, {}), + }) + async with manager: + await manager.send_function("pool", 0, "error_fn", function_with_error) + + # This should NOT hang. Before the fix, it would hang forever. + with pytest.raises(Exception): + await asyncio.wait_for( + manager.run( + pool_id="pool", + worker_id=0, + func_import_path_or_key="error_fn", + send_channel=False, + func_args=(), + func_kwargs={}, + ), + timeout=5.0, + ) + + # Worker should still be functional after the error + await manager.send_function("pool", 0, "add", add_numbers) + result = await manager.run( + pool_id="pool", + worker_id=0, + func_import_path_or_key="add", + send_channel=False, + func_args=(10, 20), + func_kwargs={}, + ) + assert result.result == 30 + +# %% +await test_worker_exception_does_not_hang(); diff --git a/netrun/pts/tests/05_execution_manager/test_execution_manager_thread.pct.py b/netrun/pts/tests/05_execution_manager/test_execution_manager_thread.pct.py index 7a4c3c82..3dc7ce55 100644 --- a/netrun/pts/tests/05_execution_manager/test_execution_manager_thread.pct.py +++ b/netrun/pts/tests/05_execution_manager/test_execution_manager_thread.pct.py @@ -31,6 +31,7 @@ add_numbers, multiply_numbers, slow_function, + function_with_error, function_returns_non_serializable, async_add, function_with_kwargs, @@ -644,3 +645,108 @@ async def test_main_pool(): # %% await test_main_pool(); + +# %% [markdown] +# ## Regression: Worker jobs cleanup on cancellation + +# %% +#|export +@pytest.mark.asyncio +async def test_worker_jobs_cleanup_on_cancellation(): + """Test that _worker_jobs is cleaned up when run() is cancelled by timeout. + + Regression test: worker jobs leak on timeout/cancellation causing + incorrect LEAST_BUSY worker selection. + """ + manager = ExecutionManager({ + "pool": (ThreadPool, {"num_workers": 2}), + }) + async with manager: + await manager.send_function_to_pool("pool", "slow", slow_function) + + # Start a slow job and cancel it via timeout + with pytest.raises((asyncio.TimeoutError, asyncio.CancelledError)): + await asyncio.wait_for( + manager.run( + pool_id="pool", + worker_id=0, + func_import_path_or_key="slow", + send_channel=False, + func_args=(30.0,), # 30s — will be cancelled long before + func_kwargs={}, + ), + timeout=0.05, + ) + + # _worker_jobs should be empty after cancellation + assert len(manager._worker_jobs[("pool", 0)]) == 0, ( + "worker_jobs leaked after cancellation" + ) + + # msg queue should also be cleaned up + pool_msgs = manager._msgs.get("pool", {}) + assert len(pool_msgs) == 0, ( + f"msg queue leaked after cancellation: {list(pool_msgs.keys())}" + ) + + # LEAST_BUSY allocation should still work correctly + await manager.send_function_to_pool("pool", "add", add_numbers) + result = await manager.run_allocate( + pool_worker_ids=["pool"], + allocation_method=RunAllocationMethod.LEAST_BUSY, + func_import_path_or_key="add", + send_channel=False, + func_args=(1, 2), + func_kwargs={}, + ) + assert result.result == 3 + +# %% +await test_worker_jobs_cleanup_on_cancellation(); + +# %% [markdown] +# ## Regression: Worker exception does not crash worker loop + +# %% +#|export +@pytest.mark.asyncio +async def test_worker_exception_does_not_crash_loop(): + """Test that an exception in a sync worker doesn't crash the worker loop. + + Regression test: exception in _func_runner crashes the while True loop + in _worker_func, killing the entire worker. + """ + manager = ExecutionManager({ + "pool": (ThreadPool, {"num_workers": 1}), + }) + async with manager: + await manager.send_function("pool", 0, "error_fn", function_with_error) + await manager.send_function("pool", 0, "add", add_numbers) + + # First call: should get an error, not hang or crash the worker + with pytest.raises(Exception): + await asyncio.wait_for( + manager.run( + pool_id="pool", + worker_id=0, + func_import_path_or_key="error_fn", + send_channel=False, + func_args=(), + func_kwargs={}, + ), + timeout=5.0, + ) + + # Worker should still be alive. Second call should succeed: + result = await manager.run( + pool_id="pool", + worker_id=0, + func_import_path_or_key="add", + send_channel=False, + func_args=(1, 2), + func_kwargs={}, + ) + assert result.result == 3 + +# %% +await test_worker_exception_does_not_crash_loop(); diff --git a/netrun/pts/tests/06_net/test_net.pct.py b/netrun/pts/tests/06_net/test_net.pct.py index 376db5e7..a553e025 100644 --- a/netrun/pts/tests/06_net/test_net.pct.py +++ b/netrun/pts/tests/06_net/test_net.pct.py @@ -7037,3 +7037,119 @@ async def node_c(ctx, packets): # %% asyncio.get_event_loop().run_until_complete(test_concurrent_async_siblings_on_main_pool()) + +# %% [markdown] +# ## Regression: faulthandler enabled after init + +# %% +#|export +@pytest.mark.asyncio +async def test_faulthandler_enabled_after_init(): + """Test that faulthandler is enabled after Net.init().""" + import faulthandler + import platform + + def noop_node(ctx, packets): + pass + + graph_config = GraphConfig( + nodes=[ + NodeConfig( + name="Noop", + in_ports={"in": PortConfig()}, + in_salvo_conditions={ + "default": SalvoConditionConfig( + max_salvos=MaxSalvosFiniteConfig(max=1), + ports={"in": PacketCountAllConfig()}, + term=SalvoConditionTermPortConfig( + port_name="in", + state=PortStateNonEmptyConfig(), + ), + ), + }, + execution_config=NodeExecutionConfig( + node_name="Noop", + pools=["main"], + exec_node_func=noop_node, + ), + ), + ], + edges=[], + ) + config = NetConfig( + pools={"main": PoolConfig(spec=MainPoolConfig())}, + graph=graph_config, + ) + + async with Net(config) as net: + assert faulthandler.is_enabled() + +# %% +asyncio.get_event_loop().run_until_complete(test_faulthandler_enabled_after_init()) + +# %% [markdown] +# ## Regression: gc.collect called between retries + +# %% +#|export +@pytest.mark.asyncio +async def test_retry_calls_gc_collect(): + """Test that gc.collect() is called between retry attempts.""" + import gc + + call_count = [0] + + def failing_then_succeeds(ctx, packets): + call_count[0] += 1 + for port_name, pkt_ids in packets.items(): + for pid in pkt_ids: + ctx.consume_packet(pid) + if call_count[0] <= 2: + raise ValueError(f"Fail attempt {call_count[0]}") + + graph_config = GraphConfig( + nodes=[ + NodeConfig( + name="RetryNode", + in_ports={"in": PortConfig()}, + in_salvo_conditions={ + "default": SalvoConditionConfig( + max_salvos=MaxSalvosFiniteConfig(max=1), + ports={"in": PacketCountAllConfig()}, + term=SalvoConditionTermPortConfig( + port_name="in", + state=PortStateNonEmptyConfig(), + ), + ), + }, + execution_config=NodeExecutionConfig( + node_name="RetryNode", + pools=["main"], + exec_node_func=failing_then_succeeds, + retries=3, + retry_wait=0.0, + ), + ), + ], + edges=[], + ) + config = NetConfig( + pools={"main": PoolConfig(spec=MainPoolConfig())}, + graph=graph_config, + ) + + with patch.object(gc, "collect", wraps=gc.collect) as mock_gc: + async with Net(config) as net: + net.inject_data("RetryNode", "in", [1]) + await net.run_until_blocked() + + # gc.collect() should have been called at least once (between retries) + assert mock_gc.call_count >= 1, ( + f"gc.collect() was not called between retries (call_count={mock_gc.call_count})" + ) + + # The node should have succeeded on 3rd attempt + assert call_count[0] == 3 + +# %% +asyncio.get_event_loop().run_until_complete(test_retry_calls_gc_collect()) diff --git a/netrun/pts/tests/10_cli/test_cli.pct.py b/netrun/pts/tests/10_cli/test_cli.pct.py index 69d7f8ee..2379f78e 100644 --- a/netrun/pts/tests/10_cli/test_cli.pct.py +++ b/netrun/pts/tests/10_cli/test_cli.pct.py @@ -410,3 +410,51 @@ def test_help(): assert "actions" in result.stdout assert "recipes" in result.stdout assert "download-agents" in result.stdout + +# %% [markdown] +# ## Regression: validate reports resolution failures + +# %% +#|export +import tempfile + +def test_validate_reports_resolution_failure(): + """Test that validate reports resolution errors instead of silently swallowing them. + + Regression test: netrun validate suppresses resolution failures by catching + all exceptions and setting resolved=None, reporting valid=True. + """ + config_data = { + "graph": { + "nodes": [ + { + "name": "TestNode", + "factory": "nonexistent_module_that_does_not_exist.factory", + "factory_args": {}, + "execution_config": { + "pools": ["main"], + }, + } + ], + "edges": [], + }, + "pools": { + "main": {"spec": {"type": "main"}}, + }, + } + with tempfile.NamedTemporaryFile(mode="w", suffix=".netrun.json", delete=False) as f: + json.dump(config_data, f) + f.flush() + result = runner.invoke(app, ["validate", "-c", f.name]) + + # Resolution failures are reported as warnings (not errors, since resolution + # depends on Python path at runtime). The key requirement: the failure must + # NOT be silently swallowed — it must appear in the output. + data = json.loads(result.stdout) + warnings = data.get("warnings", []) + errors = data.get("errors", []) + all_messages = warnings + errors + assert any( + "resolution" in str(m).lower() or "module" in str(m).lower() + for m in all_messages + ), f"validate silently swallowed resolution failure: {data}" diff --git a/netrun/src/netrun/cli/_commands.py b/netrun/src/netrun/cli/_commands.py index a5a5ddfb..c30a4d05 100644 --- a/netrun/src/netrun/cli/_commands.py +++ b/netrun/src/netrun/cli/_commands.py @@ -35,6 +35,7 @@ def validate( """Validate a netrun config file.""" config_path = find_config(config) errors: list[str] = [] + warnings: list[str] = [] node_count = 0 edge_count = 0 @@ -63,8 +64,9 @@ def validate( # Step 3: Post-resolution validation via Rust (only if resolve succeeds) try: resolved = net_config.resolve() - except Exception: + except Exception as e: resolved = None + warnings.append(f"Resolution error: {e}") if resolved is not None: try: @@ -109,10 +111,16 @@ def _check_actions(actions: list[dict], prefix: str) -> None: errors.append(f"Raw data check error: {e}") if errors: - output_json({"valid": False, "file": str(config_path), "errors": errors}, pretty) + result = {"valid": False, "file": str(config_path), "errors": errors} + if warnings: + result["warnings"] = warnings + output_json(result, pretty) raise typer.Exit(1) else: - output_json({"valid": True, "file": str(config_path), "nodes": node_count, "edges": edge_count}, pretty) + result = {"valid": True, "file": str(config_path), "nodes": node_count, "edges": edge_count} + if warnings: + result["warnings"] = warnings + output_json(result, pretty) # %% pts/netrun/10_cli/02_commands.pct.py 7 def _port_map(ports: dict) -> dict[str, str | None]: diff --git a/netrun/src/netrun/execution_manager.py b/netrun/src/netrun/execution_manager.py index 98083d9b..31690c4d 100644 --- a/netrun/src/netrun/execution_manager.py +++ b/netrun/src/netrun/execution_manager.py @@ -136,41 +136,56 @@ def _worker_func( # RUN if key == ExecutionManagerProtocolKeys.RUN.value: msg_id, func_import_path_or_key, run_id, send_channel, args, kwargs = data - if func_import_path_or_key in registered_functions: - func = registered_functions[func_import_path_or_key] - else: - module_path, func_name = func_import_path_or_key.rsplit(".", 1) - module = importlib.import_module(module_path) - func = getattr(module, func_name) - - if func_preprocessor is not None: - func = func_preprocessor(func) - - timestamp_utc_started = get_timestamp_utc() - channel.send(ExecutionManagerProtocolKeys.UP_RUN_STARTED.value, (msg_id, timestamp_utc_started)) - res = _func_runner( - channel=channel, - func=func, - send_channel=send_channel, - args=args, - kwargs=kwargs, - event_loop=event_loop, - ) - - # Call done callback if provided - if func_done_callback is not None: - if send_channel: - func_done_callback(channel, *args, **kwargs, result=res) + timestamp_utc_started = None + started_sent = False + try: + if func_import_path_or_key in registered_functions: + func = registered_functions[func_import_path_or_key] else: - func_done_callback(*args, **kwargs, result=res) - - timestamp_utc_completed = get_timestamp_utc() - if is_in_main_process: - converted_to_str, _res = False, res - else: - converted_to_str, _res = _convert_to_str_if_not_serializable(res) - - channel.send(ExecutionManagerProtocolKeys.UP_RUN_RESPONSE.value, (msg_id, timestamp_utc_started, timestamp_utc_completed, converted_to_str, _res)) + module_path, func_name = func_import_path_or_key.rsplit(".", 1) + module = importlib.import_module(module_path) + func = getattr(module, func_name) + + if func_preprocessor is not None: + func = func_preprocessor(func) + + timestamp_utc_started = get_timestamp_utc() + channel.send(ExecutionManagerProtocolKeys.UP_RUN_STARTED.value, (msg_id, timestamp_utc_started)) + started_sent = True + res = _func_runner( + channel=channel, + func=func, + send_channel=send_channel, + args=args, + kwargs=kwargs, + event_loop=event_loop, + ) + + # Call done callback if provided + if func_done_callback is not None: + if send_channel: + func_done_callback(channel, *args, **kwargs, result=res) + else: + func_done_callback(*args, **kwargs, result=res) + + timestamp_utc_completed = get_timestamp_utc() + if is_in_main_process: + converted_to_str, _res = False, res + else: + converted_to_str, _res = _convert_to_str_if_not_serializable(res) + + channel.send(ExecutionManagerProtocolKeys.UP_RUN_RESPONSE.value, (msg_id, timestamp_utc_started, timestamp_utc_completed, converted_to_str, _res)) + except Exception as e: + # Send error response so the client doesn't hang forever + timestamp_utc_error = get_timestamp_utc() + if timestamp_utc_started is None: + timestamp_utc_started = timestamp_utc_error + try: + if not started_sent: + channel.send(ExecutionManagerProtocolKeys.UP_RUN_STARTED.value, (msg_id, timestamp_utc_started)) + channel.send(ExecutionManagerProtocolKeys.UP_RUN_RESPONSE.value, (msg_id, timestamp_utc_started, timestamp_utc_error, False, e)) + except Exception: + pass # Worker loop must survive # SEND_FUNCTION elif key == ExecutionManagerProtocolKeys.SEND_FUNCTION.value: msg_id, func_key, func = data @@ -213,30 +228,38 @@ async def _async_worker_func( async def _handle_run(msg_id, func, send_channel, args, kwargs): """Execute a single RUN request and send the response.""" timestamp_utc_started = get_timestamp_utc() - await channel.send(ExecutionManagerProtocolKeys.UP_RUN_STARTED.value, (msg_id, timestamp_utc_started)) - - res = await _async_func_runner( - channel=channel, - func=func, - send_channel=send_channel, - args=args, - kwargs=kwargs, - ) - - # Call done callback if provided - if func_done_callback is not None: - if send_channel: - callback_result = func_done_callback(channel, *args, **kwargs, result=res) - else: - callback_result = func_done_callback(*args, **kwargs, result=res) - # Await if the callback is async - if asyncio.iscoroutine(callback_result): - await callback_result - - timestamp_utc_completed = get_timestamp_utc() - converted_to_str, _res = False, res + try: + await channel.send(ExecutionManagerProtocolKeys.UP_RUN_STARTED.value, (msg_id, timestamp_utc_started)) + + res = await _async_func_runner( + channel=channel, + func=func, + send_channel=send_channel, + args=args, + kwargs=kwargs, + ) - await channel.send(ExecutionManagerProtocolKeys.UP_RUN_RESPONSE.value, (msg_id, timestamp_utc_started, timestamp_utc_completed, converted_to_str, _res)) + # Call done callback if provided + if func_done_callback is not None: + if send_channel: + callback_result = func_done_callback(channel, *args, **kwargs, result=res) + else: + callback_result = func_done_callback(*args, **kwargs, result=res) + # Await if the callback is async + if asyncio.iscoroutine(callback_result): + await callback_result + + timestamp_utc_completed = get_timestamp_utc() + converted_to_str, _res = False, res + + await channel.send(ExecutionManagerProtocolKeys.UP_RUN_RESPONSE.value, (msg_id, timestamp_utc_started, timestamp_utc_completed, converted_to_str, _res)) + except Exception as e: + # Send error response so the client doesn't hang forever + timestamp_utc_error = get_timestamp_utc() + try: + await channel.send(ExecutionManagerProtocolKeys.UP_RUN_RESPONSE.value, (msg_id, timestamp_utc_started, timestamp_utc_error, False, e)) + except Exception: + pass # If we can't send the error, the client will eventually timeout pending_tasks: set[asyncio.Task] = set() @@ -460,7 +483,12 @@ async def _msg_recv_task_func(self, pool_id: str): msg = await pool.recv() msg_id = msg.data[0] msg.data = msg.data[1:] - await self._msgs[pool_id][msg_id].put(msg) + # Queue may have been cleaned up if the job was cancelled/timed out. + # Silently discard orphaned responses. + queue = self._msgs[pool_id].get(msg_id) + if queue is not None: + await queue.put(msg) + async def _send_msg(self, pool_id: str, worker_id: str, key: str, data: Any) -> str: pool = self._pools[pool_id] @@ -571,32 +599,46 @@ async def run( self._worker_round_robin_lst.remove((pool_id, worker_id)) self._worker_round_robin_lst.append((pool_id, worker_id)) - msg_id = await self._send_msg( - pool_id=pool_id, - worker_id=worker_id, - key=ExecutionManagerProtocolKeys.RUN.value, - data=(func_import_path_or_key, run_id, send_channel, func_args, func_kwargs), - ) + msg_id = None + try: + msg_id = await self._send_msg( + pool_id=pool_id, + worker_id=worker_id, + key=ExecutionManagerProtocolKeys.RUN.value, + data=(func_import_path_or_key, run_id, send_channel, func_args, func_kwargs), + ) - # Wait for UP_RUN_STARTED - started_msg = await self._recv_msg(pool_id, msg_id, expect=ExecutionManagerProtocolKeys.UP_RUN_STARTED, close_msg_queue=False) - job_info.timestamp_utc_started = started_msg.data[0] # timestamp_utc_started - - # Wait for UP_RUN_RESPONSE - msg = await self._recv_msg(pool_id, msg_id, expect=ExecutionManagerProtocolKeys.UP_RUN_RESPONSE, close_msg_queue=True) - self._worker_jobs[(pool_id, worker_id)].remove(job_info) - - timestamp_utc_started, timestamp_utc_completed, converted_to_str, _res = msg.data - return JobResult( - timestamp_utc_submitted=job_info.timestamp_utc_submitted, - timestamp_utc_started=job_info.timestamp_utc_started, - timestamp_utc_completed=timestamp_utc_completed, - func_import_path_or_key=job_info.func_import_path_or_key, - pool_id=job_info.pool_id, - worker_id=job_info.worker_id, - converted_to_str=converted_to_str, - result=_res, - ) + # Wait for UP_RUN_STARTED + started_msg = await self._recv_msg(pool_id, msg_id, expect=ExecutionManagerProtocolKeys.UP_RUN_STARTED, close_msg_queue=False) + job_info.timestamp_utc_started = started_msg.data[0] # timestamp_utc_started + + # Wait for UP_RUN_RESPONSE + msg = await self._recv_msg(pool_id, msg_id, expect=ExecutionManagerProtocolKeys.UP_RUN_RESPONSE, close_msg_queue=True) + msg_id = None # Already cleaned up by close_msg_queue=True + + self._worker_jobs[(pool_id, worker_id)].remove(job_info) + + timestamp_utc_started, timestamp_utc_completed, converted_to_str, _res = msg.data + if isinstance(_res, Exception): + raise _res + return JobResult( + timestamp_utc_submitted=job_info.timestamp_utc_submitted, + timestamp_utc_started=job_info.timestamp_utc_started, + timestamp_utc_completed=timestamp_utc_completed, + func_import_path_or_key=job_info.func_import_path_or_key, + pool_id=job_info.pool_id, + worker_id=job_info.worker_id, + converted_to_str=converted_to_str, + result=_res, + ) + finally: + # Clean up leaked state on cancellation/exception + try: + self._worker_jobs[(pool_id, worker_id)].remove(job_info) + except ValueError: + pass # Already removed on the success path + if msg_id is not None and msg_id in self._msgs.get(pool_id, {}): + del self._msgs[pool_id][msg_id] async def run_allocate( self, diff --git a/netrun/src/netrun/net/_net/_net.py b/netrun/src/netrun/net/_net/_net.py index 507db3c1..26856802 100644 --- a/netrun/src/netrun/net/_net/_net.py +++ b/netrun/src/netrun/net/_net/_net.py @@ -1233,6 +1233,15 @@ async def init(self, run_init_nodes: bool | None = None) -> None: if self._initialized: raise RuntimeError("Net already initialized") + # Enable faulthandler for debugging (SIGUSR1 -> stack dump of all threads) + import faulthandler + try: + faulthandler.enable() + if hasattr(signal, 'SIGUSR1'): + faulthandler.register(signal.SIGUSR1, all_threads=True) + except (OSError, ValueError, IOError): + pass # stderr may not support fileno (mocked in tests), or signal unavailable + await self._execution_manager.start() # Register node functions with all workers @@ -2908,6 +2917,24 @@ async def _handle_epoch_failure( if effective_retry_wait > 0: await asyncio.sleep(effective_retry_wait) + # Force garbage collection to reclaim resources from the failed attempt + import gc + gc.collect() + + # Log peak RSS for debugging memory issues during retries + try: + import resource + import platform as _platform + import logging + rss_raw = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss + rss_mb = rss_raw / (1024 * 1024) if _platform.system() == "Darwin" else rss_raw / 1024 + logging.getLogger("netrun.net").debug( + "Retry %d/%d for node '%s' (epoch %s), peak RSS: %.1f MB", + retry_count + 1, effective_retries, node_name, epoch_id, rss_mb, + ) + except ImportError: + pass # resource module not available (Windows) + # Retry (deferred actions were discarded, so we start fresh) return await self._execute_epoch_with_retry( epoch_id=epoch_id, diff --git a/netrun/src/tests/cli/test_cli.py b/netrun/src/tests/cli/test_cli.py index 63d88237..01fd2410 100644 --- a/netrun/src/tests/cli/test_cli.py +++ b/netrun/src/tests/cli/test_cli.py @@ -1,6 +1,6 @@ # AUTOGENERATED! DO NOT EDIT! File to edit: pts/tests/10_cli/test_cli.pct.py -__all__ = ['BASIC_CONFIG', 'POOLS_CONFIG', 'SAMPLE_DIR', 'runner', 'test_actions_list_basic', 'test_actions_run_global_and_node_conflict', 'test_actions_run_global_not_found', 'test_actions_run_not_found', 'test_actions_run_requires_node_or_global', 'test_convert_bad_extension', 'test_convert_json_to_toml', 'test_convert_not_found', 'test_download_agents_branch_option', 'test_download_agents_network_error', 'test_download_agents_no_files', 'test_download_agents_partial_failure', 'test_download_agents_success', 'test_factory_info', 'test_factory_info_bad_module', 'test_help', 'test_info_basic', 'test_info_pools', 'test_node_detail', 'test_node_not_found', 'test_nodes_basic', 'test_recipes_list_empty', 'test_recipes_list_pools', 'test_recipes_run_not_found', 'test_structure_basic', 'test_structure_node_has_factory', 'test_validate_basic', 'test_validate_not_found', 'test_validate_pools', 'test_validate_pretty'] +__all__ = ['BASIC_CONFIG', 'POOLS_CONFIG', 'SAMPLE_DIR', 'runner', 'test_actions_list_basic', 'test_actions_run_global_and_node_conflict', 'test_actions_run_global_not_found', 'test_actions_run_not_found', 'test_actions_run_requires_node_or_global', 'test_convert_bad_extension', 'test_convert_json_to_toml', 'test_convert_not_found', 'test_download_agents_branch_option', 'test_download_agents_network_error', 'test_download_agents_no_files', 'test_download_agents_partial_failure', 'test_download_agents_success', 'test_factory_info', 'test_factory_info_bad_module', 'test_help', 'test_info_basic', 'test_info_pools', 'test_node_detail', 'test_node_not_found', 'test_nodes_basic', 'test_recipes_list_empty', 'test_recipes_list_pools', 'test_recipes_run_not_found', 'test_structure_basic', 'test_structure_node_has_factory', 'test_validate_basic', 'test_validate_not_found', 'test_validate_pools', 'test_validate_pretty', 'test_validate_reports_resolution_failure'] # %% pts/tests/10_cli/test_cli.pct.py 2 import json @@ -353,3 +353,47 @@ def test_help(): assert "actions" in result.stdout assert "recipes" in result.stdout assert "download-agents" in result.stdout + +# %% pts/tests/10_cli/test_cli.pct.py 27 +import tempfile + +def test_validate_reports_resolution_failure(): + """Test that validate reports resolution errors instead of silently swallowing them. + + Regression test: netrun validate suppresses resolution failures by catching + all exceptions and setting resolved=None, reporting valid=True. + """ + config_data = { + "graph": { + "nodes": [ + { + "name": "TestNode", + "factory": "nonexistent_module_that_does_not_exist.factory", + "factory_args": {}, + "execution_config": { + "pools": ["main"], + }, + } + ], + "edges": [], + }, + "pools": { + "main": {"spec": {"type": "main"}}, + }, + } + with tempfile.NamedTemporaryFile(mode="w", suffix=".netrun.json", delete=False) as f: + json.dump(config_data, f) + f.flush() + result = runner.invoke(app, ["validate", "-c", f.name]) + + # Resolution failures are reported as warnings (not errors, since resolution + # depends on Python path at runtime). The key requirement: the failure must + # NOT be silently swallowed — it must appear in the output. + data = json.loads(result.stdout) + warnings = data.get("warnings", []) + errors = data.get("errors", []) + all_messages = warnings + errors + assert any( + "resolution" in str(m).lower() or "module" in str(m).lower() + for m in all_messages + ), f"validate silently swallowed resolution failure: {data}" diff --git a/netrun/src/tests/execution_manager/test_execution_manager_async.py b/netrun/src/tests/execution_manager/test_execution_manager_async.py index 1bd228dc..093a3a97 100644 --- a/netrun/src/tests/execution_manager/test_execution_manager_async.py +++ b/netrun/src/tests/execution_manager/test_execution_manager_async.py @@ -1,6 +1,6 @@ # AUTOGENERATED! DO NOT EDIT! File to edit: pts/tests/05_execution_manager/test_execution_manager_async.pct.py -__all__ = ['test_async_function', 'test_concurrent_async_execution', 'test_context_manager', 'test_double_start_raises', 'test_empty_workers_raises', 'test_function_with_kwargs', 'test_get_num_workers', 'test_get_worker_jobs_empty', 'test_immediate_close', 'test_job_result_timestamps', 'test_multiple_pools', 'test_non_serializable_result', 'test_pool_class_directly', 'test_pool_ids', 'test_round_robin_allocation', 'test_send_function_and_run', 'test_send_function_to_pool', 'test_start_and_close'] +__all__ = ['test_async_function', 'test_concurrent_async_execution', 'test_context_manager', 'test_double_start_raises', 'test_empty_workers_raises', 'test_function_with_kwargs', 'test_get_num_workers', 'test_get_worker_jobs_empty', 'test_immediate_close', 'test_job_result_timestamps', 'test_multiple_pools', 'test_non_serializable_result', 'test_pool_class_directly', 'test_pool_ids', 'test_round_robin_allocation', 'test_send_function_and_run', 'test_send_function_to_pool', 'test_start_and_close', 'test_worker_exception_does_not_hang'] # %% pts/tests/05_execution_manager/test_execution_manager_async.pct.py 2 import pytest @@ -18,6 +18,7 @@ add_numbers, multiply_numbers, slow_function, + function_with_error, function_returns_non_serializable, function_with_kwargs, async_add, @@ -433,3 +434,43 @@ async def slow_async_func(delay: float) -> str: assert elapsed < 0.5, ( f"Took {elapsed:.2f}s — expected < 0.5s for concurrent execution of 3x0.2s tasks" ) + +# %% pts/tests/05_execution_manager/test_execution_manager_async.pct.py 51 +@pytest.mark.asyncio +async def test_worker_exception_does_not_hang(): + """Test that worker-level exceptions result in an error, not a hang. + + Regression test: async worker swallows exceptions in _handle_run, + causing run() to hang forever waiting for UP_RUN_RESPONSE. + """ + manager = ExecutionManager({ + "pool": (SingleWorkerPool, {}), + }) + async with manager: + await manager.send_function("pool", 0, "error_fn", function_with_error) + + # This should NOT hang. Before the fix, it would hang forever. + with pytest.raises(Exception): + await asyncio.wait_for( + manager.run( + pool_id="pool", + worker_id=0, + func_import_path_or_key="error_fn", + send_channel=False, + func_args=(), + func_kwargs={}, + ), + timeout=5.0, + ) + + # Worker should still be functional after the error + await manager.send_function("pool", 0, "add", add_numbers) + result = await manager.run( + pool_id="pool", + worker_id=0, + func_import_path_or_key="add", + send_channel=False, + func_args=(10, 20), + func_kwargs={}, + ) + assert result.result == 30 diff --git a/netrun/src/tests/execution_manager/test_execution_manager_thread.py b/netrun/src/tests/execution_manager/test_execution_manager_thread.py index 1bb78db0..4f2734b9 100644 --- a/netrun/src/tests/execution_manager/test_execution_manager_thread.py +++ b/netrun/src/tests/execution_manager/test_execution_manager_thread.py @@ -1,6 +1,6 @@ # AUTOGENERATED! DO NOT EDIT! File to edit: pts/tests/05_execution_manager/test_execution_manager_thread.pct.py -__all__ = ['test_allocation_with_specific_workers', 'test_async_function', 'test_concurrent_jobs', 'test_context_manager', 'test_create_execution_manager', 'test_create_multiple_pools', 'test_double_start_raises', 'test_empty_workers_raises', 'test_function_with_kwargs', 'test_get_num_workers', 'test_get_worker_jobs_empty', 'test_immediate_close', 'test_invalid_pool_type', 'test_job_result_timestamps', 'test_main_pool', 'test_multiple_pools', 'test_non_serializable_result_for_main_process', 'test_pool_ids', 'test_random_allocation', 'test_round_robin_allocation', 'test_send_function_and_run', 'test_send_function_to_pool', 'test_start_and_close'] +__all__ = ['test_allocation_with_specific_workers', 'test_async_function', 'test_concurrent_jobs', 'test_context_manager', 'test_create_execution_manager', 'test_create_multiple_pools', 'test_double_start_raises', 'test_empty_workers_raises', 'test_function_with_kwargs', 'test_get_num_workers', 'test_get_worker_jobs_empty', 'test_immediate_close', 'test_invalid_pool_type', 'test_job_result_timestamps', 'test_main_pool', 'test_multiple_pools', 'test_non_serializable_result_for_main_process', 'test_pool_ids', 'test_random_allocation', 'test_round_robin_allocation', 'test_send_function_and_run', 'test_send_function_to_pool', 'test_start_and_close', 'test_worker_exception_does_not_crash_loop', 'test_worker_jobs_cleanup_on_cancellation'] # %% pts/tests/05_execution_manager/test_execution_manager_thread.pct.py 2 import pytest @@ -20,6 +20,7 @@ add_numbers, multiply_numbers, slow_function, + function_with_error, function_returns_non_serializable, async_add, function_with_kwargs, @@ -505,3 +506,94 @@ async def test_main_pool(): ) assert result.result == 300 + +# %% pts/tests/05_execution_manager/test_execution_manager_thread.pct.py 62 +@pytest.mark.asyncio +async def test_worker_jobs_cleanup_on_cancellation(): + """Test that _worker_jobs is cleaned up when run() is cancelled by timeout. + + Regression test: worker jobs leak on timeout/cancellation causing + incorrect LEAST_BUSY worker selection. + """ + manager = ExecutionManager({ + "pool": (ThreadPool, {"num_workers": 2}), + }) + async with manager: + await manager.send_function_to_pool("pool", "slow", slow_function) + + # Start a slow job and cancel it via timeout + with pytest.raises((asyncio.TimeoutError, asyncio.CancelledError)): + await asyncio.wait_for( + manager.run( + pool_id="pool", + worker_id=0, + func_import_path_or_key="slow", + send_channel=False, + func_args=(30.0,), # 30s — will be cancelled long before + func_kwargs={}, + ), + timeout=0.05, + ) + + # _worker_jobs should be empty after cancellation + assert len(manager._worker_jobs[("pool", 0)]) == 0, ( + "worker_jobs leaked after cancellation" + ) + + # msg queue should also be cleaned up + pool_msgs = manager._msgs.get("pool", {}) + assert len(pool_msgs) == 0, ( + f"msg queue leaked after cancellation: {list(pool_msgs.keys())}" + ) + + # LEAST_BUSY allocation should still work correctly + await manager.send_function_to_pool("pool", "add", add_numbers) + result = await manager.run_allocate( + pool_worker_ids=["pool"], + allocation_method=RunAllocationMethod.LEAST_BUSY, + func_import_path_or_key="add", + send_channel=False, + func_args=(1, 2), + func_kwargs={}, + ) + assert result.result == 3 + +# %% pts/tests/05_execution_manager/test_execution_manager_thread.pct.py 65 +@pytest.mark.asyncio +async def test_worker_exception_does_not_crash_loop(): + """Test that an exception in a sync worker doesn't crash the worker loop. + + Regression test: exception in _func_runner crashes the while True loop + in _worker_func, killing the entire worker. + """ + manager = ExecutionManager({ + "pool": (ThreadPool, {"num_workers": 1}), + }) + async with manager: + await manager.send_function("pool", 0, "error_fn", function_with_error) + await manager.send_function("pool", 0, "add", add_numbers) + + # First call: should get an error, not hang or crash the worker + with pytest.raises(Exception): + await asyncio.wait_for( + manager.run( + pool_id="pool", + worker_id=0, + func_import_path_or_key="error_fn", + send_channel=False, + func_args=(), + func_kwargs={}, + ), + timeout=5.0, + ) + + # Worker should still be alive. Second call should succeed: + result = await manager.run( + pool_id="pool", + worker_id=0, + func_import_path_or_key="add", + send_channel=False, + func_args=(1, 2), + func_kwargs={}, + ) + assert result.result == 3 diff --git a/netrun/src/tests/net/test_net.py b/netrun/src/tests/net/test_net.py index 5a952b9b..850ad703 100644 --- a/netrun/src/tests/net/test_net.py +++ b/netrun/src/tests/net/test_net.py @@ -1,6 +1,6 @@ # AUTOGENERATED! DO NOT EDIT! File to edit: pts/tests/06_net/test_net.pct.py -__all__ = ['create_simple_graph_config', 'create_simple_net_config', 'test_async_epoch_callback', 'test_async_error_on_thread_pool', 'test_async_exec_func_on_main_pool', 'test_async_exec_func_on_thread_pool', 'test_async_from_function_factory_on_thread_pool', 'test_async_retry_on_thread_pool', 'test_async_subprocess_on_thread_pool', 'test_check_type_distinguishes_modes', 'test_check_type_string_generic_fails', 'test_check_type_string_simple', 'test_check_type_type_object_generic', 'test_check_type_type_object_generic_mismatch', 'test_check_type_type_object_simple', 'test_close_node_func_called_on_net_stop', 'test_concurrent_async_siblings_on_main_pool', 'test_context_cancel_epoch', 'test_context_consume_packet', 'test_context_consume_packet_not_found', 'test_context_create_packet', 'test_context_create_packet_from_value_func', 'test_context_full_workflow', 'test_context_get_execution_result', 'test_context_load_output_port', 'test_context_print_accumulates', 'test_context_print_basic', 'test_context_print_custom_separators', 'test_context_print_echo_stdout', 'test_context_print_empty', 'test_context_print_flush_ignored', 'test_context_print_multiple_timestamps', 'test_context_print_non_string', 'test_context_send_output_salvo', 'test_create_func_preprocessor_from_config', 'test_create_func_preprocessor_from_config_empty', 'test_create_net_func_preprocessor_basic', 'test_create_net_func_preprocessor_captures_exception', 'test_create_net_func_preprocessor_captures_prints', 'test_create_net_func_preprocessor_with_retry_info', 'test_ctx_vars_access', 'test_ctx_vars_empty', 'test_ctx_vars_inherit_missing_global_raises', 'test_ctx_vars_merging', 'test_ctx_vars_merging_with_inherit', 'test_dead_letter_queue_after_max_retries', 'test_dead_letter_queue_has_pool_and_worker', 'test_defer_init_delays_init_node_func', 'test_deferred_actions_preserved_in_result', 'test_deferred_queue_add_consume_packet', 'test_deferred_queue_add_create_packet', 'test_deferred_queue_add_load_output_port', 'test_deferred_queue_add_send_output_salvo', 'test_deferred_queue_discard', 'test_deferred_queue_multiple_creates', 'test_enable_disable_unknown_node', 'test_epoch_callback_deregistration', 'test_epoch_cancelled_can_be_raised_and_caught', 'test_epoch_cancelled_exception', 'test_epoch_end_on_cancelled', 'test_epoch_error_basic', 'test_epoch_error_can_be_caught', 'test_epoch_error_chaining', 'test_epoch_error_minimal', 'test_epoch_error_str', 'test_epoch_execution_simple_node', 'test_epoch_execution_with_output', 'test_epoch_record_cancellation_lifecycle', 'test_epoch_record_lifecycle_timestamps', 'test_epoch_record_out_salvos_populated', 'test_exception_queue_contains_epoch_error', 'test_execute_node_calls_init_node_func', 'test_execute_node_inside_net_basic', 'test_execute_node_inside_net_source_node', 'test_execute_node_outside_net', 'test_execute_node_outside_net_async_func', 'test_execute_node_salvo_condition_not_satisfied', 'test_execution_manager_get_pool', 'test_factory_node_exec_func_override', 'test_global_max_epochs_default_unlimited', 'test_global_max_epochs_limits_all_nodes', 'test_init_node_func_called_on_net_start', 'test_init_node_func_called_once_with_defer_init', 'test_is_blocked_with_disabled_node', 'test_max_epochs_allows_up_to_limit', 'test_max_epochs_exceeded_exception', 'test_max_epochs_none_unlimited', 'test_max_epochs_one_raises_on_second', 'test_max_epochs_queued_when_not_propagating', 'test_max_parallel_epochs_limits_concurrent_starts', 'test_max_parallel_epochs_none_allows_all', 'test_max_parallel_epochs_per_node', 'test_multiple_epoch_callbacks', 'test_net_check_rate_limit_enforced', 'test_net_check_rate_limit_no_config', 'test_net_check_rate_limit_none_limit', 'test_net_check_rate_limit_window_expires', 'test_net_clear_dead_letter_queue', 'test_net_close_sync', 'test_net_config_default_pool_allocation_method', 'test_net_config_default_pool_allocation_method_default', 'test_net_config_property', 'test_net_config_type_checking', 'test_net_context_manager', 'test_net_creation', 'test_net_dead_letter_queue_empty', 'test_net_dead_letter_queue_returns_copy', 'test_net_from_file_json', 'test_net_from_file_not_found', 'test_net_from_file_unsupported_format', 'test_net_get_epoch_log_empty', 'test_net_get_node_logs_empty', 'test_net_get_startable_epochs', 'test_net_graph_property', 'test_net_handle_print_buffer', 'test_net_init_sync', 'test_net_install_sigint_handler', 'test_net_invalid_pool_type_raises', 'test_net_is_blocked_empty_network', 'test_net_is_blocked_with_running_epochs', 'test_net_pause_and_resume', 'test_net_protocol_keys_uniqueness', 'test_net_protocol_keys_values', 'test_net_run_step', 'test_net_run_until_blocked', 'test_net_running_epochs_empty', 'test_net_start_and_stop', 'test_net_start_background', 'test_net_start_background_already_running', 'test_net_start_twice_raises', 'test_net_with_multiple_pool_types', 'test_net_with_node_execution_configs', 'test_node_disable_at_runtime', 'test_node_disabled_via_config', 'test_node_enable_at_runtime', 'test_node_execution_config_defaults', 'test_node_execution_config_max_epochs_default', 'test_node_execution_config_max_epochs_set', 'test_node_execution_config_new_fields', 'test_node_execution_config_retry_defaults', 'test_node_execution_config_type_checking', 'test_node_execution_config_with_retries', 'test_node_execution_context_creation', 'test_node_execution_context_with_retry_info', 'test_node_execution_result_with_exception', 'test_node_execution_result_with_func_result', 'test_node_failure_context_creation', 'test_node_failure_context_full', 'test_node_failure_context_has_pool_and_worker', 'test_node_failure_context_print', 'test_node_level_override_print', 'test_node_level_override_propagate', 'test_node_scoped_epoch_callbacks', 'test_on_epoch_end_callback', 'test_on_epoch_start_callback', 'test_on_node_failure_callback', 'test_per_node_max_epochs_overrides_global', 'test_per_node_minus_one_overrides_global_limit', 'test_pool_server_context_as_context_manager_still_works', 'test_pool_server_context_log_no_file', 'test_pool_server_context_start_stop', 'test_pool_server_context_start_stop_with_log_file', 'test_preprocessor_handles_cancel_epoch', 'test_preprocessor_type_checking_inheritance', 'test_print_exceptions_includes_pool_worker', 'test_print_exceptions_true_prints_to_stderr', 'test_propagate_exceptions_false_queues', 'test_propagate_exceptions_true_raises', 'test_propagate_exceptions_wraps_in_epoch_error', 'test_request_pool_shutdown_end_to_end', 'test_request_pool_shutdown_non_remote_raises', 'test_retry_on_failure', 'test_run_on_init_basic', 'test_run_on_init_disabled', 'test_run_on_init_error_no_valid_condition', 'test_run_on_init_outputs_flow_downstream', 'test_run_on_init_skip_via_constructor', 'test_run_on_init_skip_via_start_param', 'test_run_on_init_start_param_overrides_constructor', 'test_serve_pool_accepts_path', 'test_serve_pool_accepts_pathlib_path', 'test_serve_pool_custom_worker_name', 'test_serve_pool_end_to_end_with_net', 'test_serve_pool_log_file_writes', 'test_serve_pool_no_log_file', 'test_serve_pool_returns_pool_server_context', 'test_serve_pool_starts_and_stops_server', 'test_serve_pool_with_log_file', 'test_server_log_callback', 'test_start_stop_with_async_funcs', 'test_stop_not_called_for_unstarted_deferred_node', 'test_streaming_downstream_starts_before_slow_sibling', 'test_streaming_error_propagates', 'test_streaming_linear_pipeline', 'test_streaming_max_parallel_epochs_across_rounds', 'test_timeout_enforcement_raises_epoch_error', 'test_timeout_goes_to_dead_letter_queue', 'test_timeout_none_no_limit', 'test_type_checking_disabled'] +__all__ = ['create_simple_graph_config', 'create_simple_net_config', 'test_async_epoch_callback', 'test_async_error_on_thread_pool', 'test_async_exec_func_on_main_pool', 'test_async_exec_func_on_thread_pool', 'test_async_from_function_factory_on_thread_pool', 'test_async_retry_on_thread_pool', 'test_async_subprocess_on_thread_pool', 'test_check_type_distinguishes_modes', 'test_check_type_string_generic_fails', 'test_check_type_string_simple', 'test_check_type_type_object_generic', 'test_check_type_type_object_generic_mismatch', 'test_check_type_type_object_simple', 'test_close_node_func_called_on_net_stop', 'test_concurrent_async_siblings_on_main_pool', 'test_context_cancel_epoch', 'test_context_consume_packet', 'test_context_consume_packet_not_found', 'test_context_create_packet', 'test_context_create_packet_from_value_func', 'test_context_full_workflow', 'test_context_get_execution_result', 'test_context_load_output_port', 'test_context_print_accumulates', 'test_context_print_basic', 'test_context_print_custom_separators', 'test_context_print_echo_stdout', 'test_context_print_empty', 'test_context_print_flush_ignored', 'test_context_print_multiple_timestamps', 'test_context_print_non_string', 'test_context_send_output_salvo', 'test_create_func_preprocessor_from_config', 'test_create_func_preprocessor_from_config_empty', 'test_create_net_func_preprocessor_basic', 'test_create_net_func_preprocessor_captures_exception', 'test_create_net_func_preprocessor_captures_prints', 'test_create_net_func_preprocessor_with_retry_info', 'test_ctx_vars_access', 'test_ctx_vars_empty', 'test_ctx_vars_inherit_missing_global_raises', 'test_ctx_vars_merging', 'test_ctx_vars_merging_with_inherit', 'test_dead_letter_queue_after_max_retries', 'test_dead_letter_queue_has_pool_and_worker', 'test_defer_init_delays_init_node_func', 'test_deferred_actions_preserved_in_result', 'test_deferred_queue_add_consume_packet', 'test_deferred_queue_add_create_packet', 'test_deferred_queue_add_load_output_port', 'test_deferred_queue_add_send_output_salvo', 'test_deferred_queue_discard', 'test_deferred_queue_multiple_creates', 'test_enable_disable_unknown_node', 'test_epoch_callback_deregistration', 'test_epoch_cancelled_can_be_raised_and_caught', 'test_epoch_cancelled_exception', 'test_epoch_end_on_cancelled', 'test_epoch_error_basic', 'test_epoch_error_can_be_caught', 'test_epoch_error_chaining', 'test_epoch_error_minimal', 'test_epoch_error_str', 'test_epoch_execution_simple_node', 'test_epoch_execution_with_output', 'test_epoch_record_cancellation_lifecycle', 'test_epoch_record_lifecycle_timestamps', 'test_epoch_record_out_salvos_populated', 'test_exception_queue_contains_epoch_error', 'test_execute_node_calls_init_node_func', 'test_execute_node_inside_net_basic', 'test_execute_node_inside_net_source_node', 'test_execute_node_outside_net', 'test_execute_node_outside_net_async_func', 'test_execute_node_salvo_condition_not_satisfied', 'test_execution_manager_get_pool', 'test_factory_node_exec_func_override', 'test_faulthandler_enabled_after_init', 'test_global_max_epochs_default_unlimited', 'test_global_max_epochs_limits_all_nodes', 'test_init_node_func_called_on_net_start', 'test_init_node_func_called_once_with_defer_init', 'test_is_blocked_with_disabled_node', 'test_max_epochs_allows_up_to_limit', 'test_max_epochs_exceeded_exception', 'test_max_epochs_none_unlimited', 'test_max_epochs_one_raises_on_second', 'test_max_epochs_queued_when_not_propagating', 'test_max_parallel_epochs_limits_concurrent_starts', 'test_max_parallel_epochs_none_allows_all', 'test_max_parallel_epochs_per_node', 'test_multiple_epoch_callbacks', 'test_net_check_rate_limit_enforced', 'test_net_check_rate_limit_no_config', 'test_net_check_rate_limit_none_limit', 'test_net_check_rate_limit_window_expires', 'test_net_clear_dead_letter_queue', 'test_net_close_sync', 'test_net_config_default_pool_allocation_method', 'test_net_config_default_pool_allocation_method_default', 'test_net_config_property', 'test_net_config_type_checking', 'test_net_context_manager', 'test_net_creation', 'test_net_dead_letter_queue_empty', 'test_net_dead_letter_queue_returns_copy', 'test_net_from_file_json', 'test_net_from_file_not_found', 'test_net_from_file_unsupported_format', 'test_net_get_epoch_log_empty', 'test_net_get_node_logs_empty', 'test_net_get_startable_epochs', 'test_net_graph_property', 'test_net_handle_print_buffer', 'test_net_init_sync', 'test_net_install_sigint_handler', 'test_net_invalid_pool_type_raises', 'test_net_is_blocked_empty_network', 'test_net_is_blocked_with_running_epochs', 'test_net_pause_and_resume', 'test_net_protocol_keys_uniqueness', 'test_net_protocol_keys_values', 'test_net_run_step', 'test_net_run_until_blocked', 'test_net_running_epochs_empty', 'test_net_start_and_stop', 'test_net_start_background', 'test_net_start_background_already_running', 'test_net_start_twice_raises', 'test_net_with_multiple_pool_types', 'test_net_with_node_execution_configs', 'test_node_disable_at_runtime', 'test_node_disabled_via_config', 'test_node_enable_at_runtime', 'test_node_execution_config_defaults', 'test_node_execution_config_max_epochs_default', 'test_node_execution_config_max_epochs_set', 'test_node_execution_config_new_fields', 'test_node_execution_config_retry_defaults', 'test_node_execution_config_type_checking', 'test_node_execution_config_with_retries', 'test_node_execution_context_creation', 'test_node_execution_context_with_retry_info', 'test_node_execution_result_with_exception', 'test_node_execution_result_with_func_result', 'test_node_failure_context_creation', 'test_node_failure_context_full', 'test_node_failure_context_has_pool_and_worker', 'test_node_failure_context_print', 'test_node_level_override_print', 'test_node_level_override_propagate', 'test_node_scoped_epoch_callbacks', 'test_on_epoch_end_callback', 'test_on_epoch_start_callback', 'test_on_node_failure_callback', 'test_per_node_max_epochs_overrides_global', 'test_per_node_minus_one_overrides_global_limit', 'test_pool_server_context_as_context_manager_still_works', 'test_pool_server_context_log_no_file', 'test_pool_server_context_start_stop', 'test_pool_server_context_start_stop_with_log_file', 'test_preprocessor_handles_cancel_epoch', 'test_preprocessor_type_checking_inheritance', 'test_print_exceptions_includes_pool_worker', 'test_print_exceptions_true_prints_to_stderr', 'test_propagate_exceptions_false_queues', 'test_propagate_exceptions_true_raises', 'test_propagate_exceptions_wraps_in_epoch_error', 'test_request_pool_shutdown_end_to_end', 'test_request_pool_shutdown_non_remote_raises', 'test_retry_calls_gc_collect', 'test_retry_on_failure', 'test_run_on_init_basic', 'test_run_on_init_disabled', 'test_run_on_init_error_no_valid_condition', 'test_run_on_init_outputs_flow_downstream', 'test_run_on_init_skip_via_constructor', 'test_run_on_init_skip_via_start_param', 'test_run_on_init_start_param_overrides_constructor', 'test_serve_pool_accepts_path', 'test_serve_pool_accepts_pathlib_path', 'test_serve_pool_custom_worker_name', 'test_serve_pool_end_to_end_with_net', 'test_serve_pool_log_file_writes', 'test_serve_pool_no_log_file', 'test_serve_pool_returns_pool_server_context', 'test_serve_pool_starts_and_stops_server', 'test_serve_pool_with_log_file', 'test_server_log_callback', 'test_start_stop_with_async_funcs', 'test_stop_not_called_for_unstarted_deferred_node', 'test_streaming_downstream_starts_before_slow_sibling', 'test_streaming_error_propagates', 'test_streaming_linear_pipeline', 'test_streaming_max_parallel_epochs_across_rounds', 'test_timeout_enforcement_raises_epoch_error', 'test_timeout_goes_to_dead_letter_queue', 'test_timeout_none_no_limit', 'test_type_checking_disabled'] # %% pts/tests/06_net/test_net.pct.py 2 import pytest @@ -6370,3 +6370,105 @@ async def node_c(ctx, packets): assert latest_start < earliest_end, ( "Nodes should have overlapping execution windows (concurrent), but they don't" ) + +# %% pts/tests/06_net/test_net.pct.py 354 +@pytest.mark.asyncio +async def test_faulthandler_enabled_after_init(): + """Test that faulthandler is enabled after Net.init().""" + import faulthandler + import platform + + def noop_node(ctx, packets): + pass + + graph_config = GraphConfig( + nodes=[ + NodeConfig( + name="Noop", + in_ports={"in": PortConfig()}, + in_salvo_conditions={ + "default": SalvoConditionConfig( + max_salvos=MaxSalvosFiniteConfig(max=1), + ports={"in": PacketCountAllConfig()}, + term=SalvoConditionTermPortConfig( + port_name="in", + state=PortStateNonEmptyConfig(), + ), + ), + }, + execution_config=NodeExecutionConfig( + node_name="Noop", + pools=["main"], + exec_node_func=noop_node, + ), + ), + ], + edges=[], + ) + config = NetConfig( + pools={"main": PoolConfig(spec=MainPoolConfig())}, + graph=graph_config, + ) + + async with Net(config) as net: + assert faulthandler.is_enabled() + +# %% pts/tests/06_net/test_net.pct.py 357 +@pytest.mark.asyncio +async def test_retry_calls_gc_collect(): + """Test that gc.collect() is called between retry attempts.""" + import gc + + call_count = [0] + + def failing_then_succeeds(ctx, packets): + call_count[0] += 1 + for port_name, pkt_ids in packets.items(): + for pid in pkt_ids: + ctx.consume_packet(pid) + if call_count[0] <= 2: + raise ValueError(f"Fail attempt {call_count[0]}") + + graph_config = GraphConfig( + nodes=[ + NodeConfig( + name="RetryNode", + in_ports={"in": PortConfig()}, + in_salvo_conditions={ + "default": SalvoConditionConfig( + max_salvos=MaxSalvosFiniteConfig(max=1), + ports={"in": PacketCountAllConfig()}, + term=SalvoConditionTermPortConfig( + port_name="in", + state=PortStateNonEmptyConfig(), + ), + ), + }, + execution_config=NodeExecutionConfig( + node_name="RetryNode", + pools=["main"], + exec_node_func=failing_then_succeeds, + retries=3, + retry_wait=0.0, + ), + ), + ], + edges=[], + ) + config = NetConfig( + pools={"main": PoolConfig(spec=MainPoolConfig())}, + graph=graph_config, + ) + + with patch.object(gc, "collect", wraps=gc.collect) as mock_gc: + async with Net(config) as net: + net.inject_data("RetryNode", "in", [1]) + await net.run_until_blocked() + + # gc.collect() should have been called at least once (between retries) + assert mock_gc.call_count >= 1, ( + f"gc.collect() was not called between retries (call_count={mock_gc.call_count})" + ) + + # The node should have succeeded on 3rd attempt + assert call_count[0] == 3 From 714b5db90b21260dab3bf2ca9604c454dbc7de23 Mon Sep 17 00:00:00 2001 From: Lukas Kikuchi Date: Sat, 25 Apr 2026 16:30:56 +0100 Subject: [PATCH 02/15] feat(netrun): replace per-thread event loops with shared async loop Phase 1 of the major refactor. Thread pool workers no longer create per-worker event loops. Instead, a single shared event loop runs on a dedicated daemon thread per process. Async node functions are submitted via run_coroutine_threadsafe(), fixing three classes of production bugs: - asyncio.create_subprocess_exec no longer hangs (shared loop has child watcher) - Nested async works naturally (no nested run_until_complete) - asyncio.Lock/Event work across concurrent workers (same event loop) Key changes: - NetFuncPreprocessor gets dual wrapper: __call__ (async, for SingleWorkerPool) and call_for_sync_worker (sync, for ThreadPool/MultiprocessPool) - Sync user functions run directly in worker threads (no event loop overhead) - Async user functions dispatch to shared loop, worker thread blocks on result - ExecutionManager creates/owns the shared loop lifecycle - Each multiprocess subprocess creates its own process-local shared loop - _func_runner falls back to shared loop for async when no preprocessor is set Co-Authored-By: Claude Opus 4.6 (1M context) --- .../pts/netrun/04_pool/02_multiprocess.pct.py | 32 ++- netrun/pts/netrun/05_execution_manager.pct.py | 215 ++++++++++------- .../netrun/06_net/01_net/00_context.pct.py | 222 ++++++++++++------ .../test_execution_manager_thread.pct.py | 195 +++++++++++++++ .../tests/05_execution_manager/workers.pct.py | 28 +++ netrun/src/netrun/execution_manager.py | 215 ++++++++++------- netrun/src/netrun/net/_net/_context.py | 222 ++++++++++++------ netrun/src/netrun/pool/multiprocess.py | 32 ++- .../test_execution_manager_thread.py | 174 +++++++++++++- netrun/src/tests/execution_manager/workers.py | 30 ++- 10 files changed, 1053 insertions(+), 312 deletions(-) diff --git a/netrun/pts/netrun/04_pool/02_multiprocess.pct.py b/netrun/pts/netrun/04_pool/02_multiprocess.pct.py index 5e0dcd61..7c09458b 100644 --- a/netrun/pts/netrun/04_pool/02_multiprocess.pct.py +++ b/netrun/pts/netrun/04_pool/02_multiprocess.pct.py @@ -272,6 +272,16 @@ def _subprocess_main_inner( # Create channel to parent parent_channel = SyncProcessChannel(parent_send_q, parent_recv_q) + # Create process-local shared event loop for async function dispatch. + # Each subprocess gets its own loop (event loops can't cross process boundaries). + shared_loop = asyncio.new_event_loop() + shared_loop_thread = threading.Thread( + target=shared_loop.run_forever, + daemon=True, + name=f"netrun-subprocess-{process_idx}-async-loop", + ) + shared_loop_thread.start() + # Create thread-safe queues for each worker # worker_queues[thread_idx] = (send_to_worker, recv_from_worker) worker_send_queues: list[queue.Queue] = [queue.Queue() for _ in range(num_threads)] @@ -284,6 +294,7 @@ def _subprocess_main_inner( t = threading.Thread( target=_thread_worker, args=(worker_fn, worker_send_queues[thread_idx], response_queue, worker_id), + kwargs={"shared_loop": shared_loop}, daemon=True, ) t.start() @@ -403,6 +414,11 @@ def output_flusher(): except Exception: pass + # Stop the process-local shared event loop + shared_loop.call_soon_threadsafe(shared_loop.stop) + shared_loop_thread.join(timeout=5.0) + shared_loop.close() + # Signal that shutdown is complete try: parent_channel.send(MP_UP_SHUTDOWN_COMPLETE, None) @@ -416,6 +432,7 @@ def _thread_worker( recv_queue: queue.Queue, response_queue: queue.Queue, worker_id: WorkerId, + shared_loop: asyncio.AbstractEventLoop | None = None, ): """Run worker function in a thread within subprocess.""" @@ -463,7 +480,20 @@ def is_closed(self) -> bool: channel = _WorkerChannel() try: - worker_fn(channel, worker_id) + # Only pass shared_loop if the worker function accepts it. + # ExecutionManager worker functions accept it; raw pool worker functions don't. + import inspect + _accepts_shared_loop = False + if shared_loop is not None: + try: + sig = inspect.signature(worker_fn) + _accepts_shared_loop = "shared_loop" in sig.parameters + except (ValueError, TypeError): + pass + if _accepts_shared_loop: + worker_fn(channel, worker_id, shared_loop=shared_loop) + else: + worker_fn(channel, worker_id) except ChannelClosed: pass except Exception as e: diff --git a/netrun/pts/netrun/05_execution_manager.pct.py b/netrun/pts/netrun/05_execution_manager.pct.py index 467320f4..a65820fc 100644 --- a/netrun/pts/netrun/05_execution_manager.pct.py +++ b/netrun/pts/netrun/05_execution_manager.pct.py @@ -27,6 +27,7 @@ from netrun._iutils import get_timestamp_utc from datetime import datetime import asyncio +import threading from enum import Enum from dataclasses import dataclass import importlib @@ -74,18 +75,33 @@ def _func_runner( send_channel: bool, args: tuple, kwargs: dict, - event_loop: asyncio.AbstractEventLoop, + shared_loop: asyncio.AbstractEventLoop | None = None, ) -> Any: + """Run a function (sync or async) in a worker thread. + + When a func_preprocessor is used, it returns a sync wrapper that handles + async dispatch internally. When called directly without a preprocessor, + async functions are submitted to the shared event loop. + """ + if send_channel: + call_args = (channel, *args) + else: + call_args = args + if asyncio.iscoroutinefunction(func): - if send_channel: - return event_loop.run_until_complete(func(channel, *args, **kwargs)) + coro = func(*call_args, **kwargs) + if shared_loop is not None: + future = asyncio.run_coroutine_threadsafe(coro, shared_loop) + return future.result() else: - return event_loop.run_until_complete(func(*args, **kwargs)) + # Fallback: create a temporary event loop (for backward compat) + loop = asyncio.new_event_loop() + try: + return loop.run_until_complete(coro) + finally: + loop.close() else: - if send_channel: - return func(channel, *args, **kwargs) - else: - return func(*args, **kwargs) + return func(*call_args, **kwargs) # %% [markdown] # ## Helpers @@ -150,6 +166,7 @@ def _worker_func( worker_id, func_preprocessor: Callable | None = None, func_done_callback: Callable | None = None, + shared_loop: asyncio.AbstractEventLoop | None = None, ): """Worker function that handles execution manager protocol messages. @@ -157,89 +174,87 @@ def _worker_func( is_in_main_process: If True, results don't need to be serializable. channel: RPC channel for communication. worker_id: ID of this worker. - func_preprocessor: If provided, this is a function that preprocesses the function before it is executed. - func_done_callback: If provided, this is called after function execution with the same args/kwargs - that were passed to the function, plus the result. Signature: callback(channel, *args, **kwargs, result=result) - if send_channel was True, otherwise callback(*args, **kwargs, result=result). + func_preprocessor: If provided, preprocesses the function before execution. + Uses call_for_sync_worker() for sync dispatch with shared_loop for async. + func_done_callback: If provided, called after function execution. + shared_loop: Shared event loop for async function dispatch. Async user + functions are submitted to this loop via run_coroutine_threadsafe(). """ + # Set shared loop on preprocessor so its sync wrapper can dispatch async functions + if func_preprocessor is not None and shared_loop is not None: + func_preprocessor._shared_loop = shared_loop - event_loop = asyncio.new_event_loop() - asyncio.set_event_loop(event_loop) registered_functions: dict[str, Callable[..., Awaitable] | Callable[..., None]] = {} - try: - while True: - key, data = channel.recv() - # RUN - if key == ExecutionManagerProtocolKeys.RUN.value: - msg_id, func_import_path_or_key, run_id, send_channel, args, kwargs = data - timestamp_utc_started = None - started_sent = False - try: - if func_import_path_or_key in registered_functions: - func = registered_functions[func_import_path_or_key] - else: - module_path, func_name = func_import_path_or_key.rsplit(".", 1) - module = importlib.import_module(module_path) - func = getattr(module, func_name) - - if func_preprocessor is not None: - func = func_preprocessor(func) - - timestamp_utc_started = get_timestamp_utc() - channel.send(ExecutionManagerProtocolKeys.UP_RUN_STARTED.value, (msg_id, timestamp_utc_started)) - started_sent = True - res = _func_runner( - channel=channel, - func=func, - send_channel=send_channel, - args=args, - kwargs=kwargs, - event_loop=event_loop, - ) - - # Call done callback if provided - if func_done_callback is not None: - if send_channel: - func_done_callback(channel, *args, **kwargs, result=res) - else: - func_done_callback(*args, **kwargs, result=res) - - timestamp_utc_completed = get_timestamp_utc() - if is_in_main_process: - converted_to_str, _res = False, res + while True: + key, data = channel.recv() + # RUN + if key == ExecutionManagerProtocolKeys.RUN.value: + msg_id, func_import_path_or_key, run_id, send_channel, args, kwargs = data + timestamp_utc_started = None + started_sent = False + try: + if func_import_path_or_key in registered_functions: + func = registered_functions[func_import_path_or_key] + else: + module_path, func_name = func_import_path_or_key.rsplit(".", 1) + module = importlib.import_module(module_path) + func = getattr(module, func_name) + + if func_preprocessor is not None: + func = func_preprocessor.call_for_sync_worker(func) + + timestamp_utc_started = get_timestamp_utc() + channel.send(ExecutionManagerProtocolKeys.UP_RUN_STARTED.value, (msg_id, timestamp_utc_started)) + started_sent = True + res = _func_runner( + channel=channel, + func=func, + send_channel=send_channel, + args=args, + kwargs=kwargs, + shared_loop=shared_loop, + ) + + # Call done callback if provided + if func_done_callback is not None: + if send_channel: + func_done_callback(channel, *args, **kwargs, result=res) else: - converted_to_str, _res = _convert_to_str_if_not_serializable(res) - - channel.send(ExecutionManagerProtocolKeys.UP_RUN_RESPONSE.value, (msg_id, timestamp_utc_started, timestamp_utc_completed, converted_to_str, _res)) - except Exception as e: - # Send error response so the client doesn't hang forever - timestamp_utc_error = get_timestamp_utc() - if timestamp_utc_started is None: - timestamp_utc_started = timestamp_utc_error - try: - if not started_sent: - channel.send(ExecutionManagerProtocolKeys.UP_RUN_STARTED.value, (msg_id, timestamp_utc_started)) - channel.send(ExecutionManagerProtocolKeys.UP_RUN_RESPONSE.value, (msg_id, timestamp_utc_started, timestamp_utc_error, False, e)) - except Exception: - pass # Worker loop must survive - # SEND_FUNCTION - elif key == ExecutionManagerProtocolKeys.SEND_FUNCTION.value: - msg_id, func_key, func = data - registered_functions[func_key] = func - channel.send(ExecutionManagerProtocolKeys.UP_SEND_FUNCTION_RESPONSE.value, (msg_id,)) - else: - raise ValueError(f"Unknown execution manager protocol key: '{key}'.") - finally: - event_loop.close() - asyncio.set_event_loop(None) + func_done_callback(*args, **kwargs, result=res) + + timestamp_utc_completed = get_timestamp_utc() + if is_in_main_process: + converted_to_str, _res = False, res + else: + converted_to_str, _res = _convert_to_str_if_not_serializable(res) -def _thread_worker_func(channel, worker_id, func_preprocessor: Callable | None = None, func_done_callback: Callable | None = None): - return _worker_func(is_in_main_process=True, channel=channel, worker_id=worker_id, func_preprocessor=func_preprocessor, func_done_callback=func_done_callback) + channel.send(ExecutionManagerProtocolKeys.UP_RUN_RESPONSE.value, (msg_id, timestamp_utc_started, timestamp_utc_completed, converted_to_str, _res)) + except Exception as e: + # Send error response so the client doesn't hang forever + timestamp_utc_error = get_timestamp_utc() + if timestamp_utc_started is None: + timestamp_utc_started = timestamp_utc_error + try: + if not started_sent: + channel.send(ExecutionManagerProtocolKeys.UP_RUN_STARTED.value, (msg_id, timestamp_utc_started)) + channel.send(ExecutionManagerProtocolKeys.UP_RUN_RESPONSE.value, (msg_id, timestamp_utc_started, timestamp_utc_error, False, e)) + except Exception: + pass # Worker loop must survive + # SEND_FUNCTION + elif key == ExecutionManagerProtocolKeys.SEND_FUNCTION.value: + msg_id, func_key, func = data + registered_functions[func_key] = func + channel.send(ExecutionManagerProtocolKeys.UP_SEND_FUNCTION_RESPONSE.value, (msg_id,)) + else: + raise ValueError(f"Unknown execution manager protocol key: '{key}'.") + +def _thread_worker_func(channel, worker_id, func_preprocessor: Callable | None = None, func_done_callback: Callable | None = None, shared_loop: asyncio.AbstractEventLoop | None = None): + return _worker_func(is_in_main_process=True, channel=channel, worker_id=worker_id, func_preprocessor=func_preprocessor, func_done_callback=func_done_callback, shared_loop=shared_loop) # If the worker is in a multiprocess pool, then the result needs to be pickleable for it to be sent back without being converted as `str(result)`. -def _multiprocess_worker_func(channel, worker_id, func_preprocessor: Callable | None = None, func_done_callback: Callable | None = None): - return _worker_func(is_in_main_process=False, channel=channel, worker_id=worker_id, func_preprocessor=func_preprocessor, func_done_callback=func_done_callback) +def _multiprocess_worker_func(channel, worker_id, func_preprocessor: Callable | None = None, func_done_callback: Callable | None = None, shared_loop: asyncio.AbstractEventLoop | None = None): + return _worker_func(is_in_main_process=False, channel=channel, worker_id=worker_id, func_preprocessor=func_preprocessor, func_done_callback=func_done_callback, shared_loop=shared_loop) # %% #|exporti @@ -348,6 +363,7 @@ def remote_execution_manager_worker( worker_id: int, func_preprocessor: Callable | None = None, func_done_callback: Callable | None = None, + shared_loop: asyncio.AbstractEventLoop | None = None, ) -> None: return _worker_func( is_in_main_process=False, @@ -355,6 +371,7 @@ def remote_execution_manager_worker( worker_id=worker_id, func_preprocessor=func_preprocessor, func_done_callback=func_done_callback, + shared_loop=shared_loop, ) # %% @@ -484,11 +501,29 @@ def __init__(self, pool_configs: dict[str, tuple[type[PoolType], dict[str, Any]] self._worker_jobs: dict[tuple[str, str], list[SubmittedJobInfo]] = {} # (pool_id, worker_id) -> list of SubmittedJobInfo self._worker_round_robin_lst: list[tuple[str, str]] = [] + # Shared event loop for async function dispatch in thread/multiprocess workers. + # Created in start(), cleaned up in close(). + self._shared_loop: asyncio.AbstractEventLoop | None = None + self._shared_loop_thread: threading.Thread | None = None + async def start(self) -> None: """Start all pools and initialize the execution manager.""" if self._started: raise RuntimeError("ExecutionManager is already started.") + # Create shared event loop on a dedicated daemon thread. + # Thread pool workers submit async coroutines to this loop via + # run_coroutine_threadsafe(). This replaces the broken per-thread + # event loops that lacked child watchers and couldn't share asyncio + # primitives across workers. + self._shared_loop = asyncio.new_event_loop() + self._shared_loop_thread = threading.Thread( + target=self._shared_loop.run_forever, + daemon=True, + name="netrun-shared-async-loop", + ) + self._shared_loop_thread.start() + for pool_id, (pool_type, pool_init_kwargs) in self._pool_configs.items(): if 'worker_fn' in pool_init_kwargs: raise ValueError("The 'worker_fn' argument should not be specified in the pool config.") @@ -498,14 +533,17 @@ async def start(self) -> None: func_done_callback = pool_kwargs.pop('func_done_callback', None) if pool_type == ThreadPool: - worker_fn = functools.partial(_thread_worker_func, func_preprocessor=func_preprocessor, func_done_callback=func_done_callback) + worker_fn = functools.partial(_thread_worker_func, func_preprocessor=func_preprocessor, func_done_callback=func_done_callback, shared_loop=self._shared_loop) self._pools[pool_id] = ThreadPool(**pool_kwargs, worker_fn=worker_fn) elif pool_type == MultiprocessPool: + # Multiprocess workers create their own process-local shared loop + # (event loops can't cross process boundaries) worker_fn = functools.partial(_multiprocess_worker_func, func_preprocessor=func_preprocessor, func_done_callback=func_done_callback) self._pools[pool_id] = MultiprocessPool(**pool_kwargs, worker_fn=worker_fn) elif pool_type == RemotePoolClient: self._pools[pool_id] = RemotePoolClient(**pool_kwargs) elif pool_type == SingleWorkerPool: + # SingleWorkerPool runs on the main event loop — no shared loop needed worker_fn = functools.partial(_async_worker_func, func_preprocessor=func_preprocessor, func_done_callback=func_done_callback) self._pools[pool_id] = SingleWorkerPool(**pool_kwargs, worker_fn=worker_fn) else: @@ -807,6 +845,15 @@ async def close(self): for pool in self._pools.values(): await pool.close() + # Stop the shared event loop + if self._shared_loop is not None: + self._shared_loop.call_soon_threadsafe(self._shared_loop.stop) + if self._shared_loop_thread is not None: + self._shared_loop_thread.join(timeout=5.0) + self._shared_loop.close() + self._shared_loop = None + self._shared_loop_thread = None + self._started = False # Propagate any errors from the recv tasks diff --git a/netrun/pts/netrun/06_net/01_net/00_context.pct.py b/netrun/pts/netrun/06_net/01_net/00_context.pct.py index 03b5b3a7..ce515516 100644 --- a/netrun/pts/netrun/06_net/01_net/00_context.pct.py +++ b/netrun/pts/netrun/06_net/01_net/00_context.pct.py @@ -1202,6 +1202,12 @@ class NetFuncPreprocessor: Unlike the closure-based version, this class can be pickled for multiprocess pools. It resolves factory-based node functions lazily on each worker. + + Provides two wrapper modes: + - __call__: returns async wrapper (for SingleWorkerPool / async contexts) + - call_for_sync_worker: returns sync wrapper (for ThreadPool / MultiprocessPool) + Async user functions are dispatched to _shared_loop via run_coroutine_threadsafe. + Sync user functions run directly in the calling thread. """ def __init__( @@ -1216,6 +1222,8 @@ def __init__( self._node_configs = node_configs # Worker-local cache for resolved factory functions self._resolved_funcs: dict[str, Callable] = {} + # Shared event loop for async dispatch (set by worker on startup) + self._shared_loop: asyncio.AbstractEventLoop | None = None def _resolve_factory(self, node_name: str) -> Callable | None: """Resolve factory function on worker (lazy). @@ -1243,13 +1251,103 @@ def _resolve_factory(self, node_name: str) -> Callable | None: self._resolved_funcs[node_name] = exec_func return exec_func + def _setup_context( + self, + exec_node_func: Callable, + epoch_id: str, + node_name: str, + packets: dict[str, list[str]], + packet_values: dict[str, Any], + retry_count: int, + retry_timestamps: list[datetime] | None, + retry_exceptions: list[Exception] | None, + ) -> tuple[NodeExecutionContext, Callable]: + """Common setup for both sync and async wrappers. + + Returns: + (ctx, actual_func) — the execution context and resolved function. + """ + config = self._node_configs.get(node_name) + in_ports = {} + out_ports = {} + if config: + in_ports = { + name: PortConfig.model_validate(port_dict) + for name, port_dict in config.in_ports.items() + } + out_ports = { + name: PortConfig.model_validate(port_dict) + for name, port_dict in config.out_ports.items() + } + + # Create execution config for context (with essential fields) + exec_config = None + if config: + exec_config = NodeExecutionConfig( + capture_prints=config.capture_prints, + print_flush_interval=config.print_flush_interval, + print_buffer_max_size=config.print_buffer_max_size, + print_echo_stdout=config.print_echo_stdout, + retries=config.retries, + retry_wait=config.retry_wait, + timeout=config.timeout, + ) + + # Reconstruct NodeVariable objects from serialized dicts + _node_vars = None + if config and config.node_vars: + _node_vars = { + n: NodeVariable.model_validate(v) + for n, v in config.node_vars.items() + } + + # Get type checking enabled flag (default True if no config) + type_checking_enabled = config.type_checking_enabled if config else True + + ctx = NodeExecutionContext( + epoch_id=epoch_id, + node_name=node_name, + retry_count=retry_count, + retry_timestamps=retry_timestamps or [], + retry_exceptions=retry_exceptions or [], + _config=exec_config, + _input_packet_values=packet_values, + _in_ports=in_ports, + _out_ports=out_ports, + _node_vars=_node_vars, + _type_checking_enabled=type_checking_enabled, + ) + + # Determine the actual function to call + actual_func = exec_node_func + if isinstance(exec_node_func, _FactoryPlaceholder): + resolved = self._resolve_factory(node_name) + if resolved is None: + raise RuntimeError( + f"Failed to resolve factory function for node '{node_name}'" + ) + actual_func = resolved + + return ctx, actual_func + + @staticmethod + def _build_result( + ctx: NodeExecutionContext, + func_result: Any, + exception: Exception | None, + ) -> NodeExecutionResult: + """Build NodeExecutionResult from context and execution outcome.""" + result = ctx._get_execution_result() + result.func_result = func_result + result.exception = exception + return result + def __call__(self, exec_node_func: Callable) -> Callable: - """Transform exec_node_func -> wrapped function. + """Return an async wrapper for SingleWorkerPool (async context). - If exec_node_func is a _FactoryPlaceholder, resolves the actual function - from the factory on this worker. + The wrapper awaits async user functions and calls sync ones directly. """ - preprocessor_self = self # Capture for inner function + preprocessor_self = self async def wrapped( epoch_id: str, @@ -1260,90 +1358,74 @@ async def wrapped( retry_timestamps: list[datetime] | None = None, retry_exceptions: list[Exception] | None = None, ) -> NodeExecutionResult: - config = preprocessor_self._node_configs.get(node_name) - in_ports = {} - out_ports = {} - if config: - in_ports = { - name: PortConfig.model_validate(port_dict) - for name, port_dict in config.in_ports.items() - } - out_ports = { - name: PortConfig.model_validate(port_dict) - for name, port_dict in config.out_ports.items() - } + ctx, actual_func = preprocessor_self._setup_context( + exec_node_func, epoch_id, node_name, packets, packet_values, + retry_count, retry_timestamps, retry_exceptions, + ) - # Create execution config for context (with essential fields) - exec_config = None - if config: - exec_config = NodeExecutionConfig( - capture_prints=config.capture_prints, - print_flush_interval=config.print_flush_interval, - print_buffer_max_size=config.print_buffer_max_size, - print_echo_stdout=config.print_echo_stdout, - retries=config.retries, - retry_wait=config.retry_wait, - timeout=config.timeout, - ) + func_result = None + exception = None - # Reconstruct NodeVariable objects from serialized dicts - _node_vars = None - if config and config.node_vars: - _node_vars = { - n: NodeVariable.model_validate(v) - for n, v in config.node_vars.items() - } + try: + ctx._validate_input_packets(packets) + if asyncio.iscoroutinefunction(actual_func): + func_result = await actual_func(ctx, packets) + else: + func_result = actual_func(ctx, packets) + except EpochCancelled: + pass + except Exception as e: + exception = e - # Get type checking enabled flag (default True if no config) - type_checking_enabled = config.type_checking_enabled if config else True - - ctx = NodeExecutionContext( - epoch_id=epoch_id, - node_name=node_name, - retry_count=retry_count, - retry_timestamps=retry_timestamps or [], - retry_exceptions=retry_exceptions or [], - _config=exec_config, - _input_packet_values=packet_values, - _in_ports=in_ports, - _out_ports=out_ports, - _node_vars=_node_vars, - _type_checking_enabled=type_checking_enabled, - ) + return preprocessor_self._build_result(ctx, func_result, exception) + + return wrapped + + def call_for_sync_worker(self, exec_node_func: Callable) -> Callable: + """Return a sync wrapper for ThreadPool/MultiprocessPool (sync context). - # Determine the actual function to call - actual_func = exec_node_func - if isinstance(exec_node_func, _FactoryPlaceholder): - # Resolve factory function on this worker - resolved = preprocessor_self._resolve_factory(node_name) - if resolved is None: - raise RuntimeError( - f"Failed to resolve factory function for node '{node_name}'" - ) - actual_func = resolved + Sync user functions run directly in the calling worker thread. + Async user functions are dispatched to the shared event loop via + run_coroutine_threadsafe and the worker thread blocks on the result. + """ + preprocessor_self = self + + def wrapped( + epoch_id: str, + node_name: str, + packets: dict[str, list[str]], + packet_values: dict[str, Any], + retry_count: int = 0, + retry_timestamps: list[datetime] | None = None, + retry_exceptions: list[Exception] | None = None, + ) -> NodeExecutionResult: + ctx, actual_func = preprocessor_self._setup_context( + exec_node_func, epoch_id, node_name, packets, packet_values, + retry_count, retry_timestamps, retry_exceptions, + ) func_result = None exception = None try: - # Validate input packet types before executing the node function ctx._validate_input_packets(packets) if asyncio.iscoroutinefunction(actual_func): - func_result = await actual_func(ctx, packets) + if preprocessor_self._shared_loop is None: + raise RuntimeError( + f"Async node function '{node_name}' requires a shared event loop, " + "but none was set on the preprocessor. This is a netrun internal error." + ) + coro = actual_func(ctx, packets) + future = asyncio.run_coroutine_threadsafe(coro, preprocessor_self._shared_loop) + func_result = future.result() else: func_result = actual_func(ctx, packets) except EpochCancelled: - # Expected when ctx.cancel_epoch() is called pass except Exception as e: exception = e - # Get the execution result with all deferred actions - result = ctx._get_execution_result() - result.func_result = func_result - result.exception = exception - - return result + return preprocessor_self._build_result(ctx, func_result, exception) return wrapped diff --git a/netrun/pts/tests/05_execution_manager/test_execution_manager_thread.pct.py b/netrun/pts/tests/05_execution_manager/test_execution_manager_thread.pct.py index 3dc7ce55..ce0b37a1 100644 --- a/netrun/pts/tests/05_execution_manager/test_execution_manager_thread.pct.py +++ b/netrun/pts/tests/05_execution_manager/test_execution_manager_thread.pct.py @@ -35,6 +35,8 @@ function_returns_non_serializable, async_add, function_with_kwargs, + async_subprocess_function, + nested_async_function, ) # %% [markdown] @@ -750,3 +752,196 @@ async def test_worker_exception_does_not_crash_loop(): # %% await test_worker_exception_does_not_crash_loop(); + +# %% [markdown] +# ## Shared Event Loop Tests + +# %% +#|export +@pytest.mark.asyncio +async def test_async_subprocess_in_thread_pool(): + """Test that async subprocess works on ThreadPool via shared event loop. + + Previously, per-thread event loops lacked child watchers, causing + asyncio.create_subprocess_exec to hang forever. The shared event loop + fixes this. + """ + manager = ExecutionManager({ + "pool": (ThreadPool, {"num_workers": 1}), + }) + async with manager: + await manager.send_function("pool", 0, "subprocess_fn", async_subprocess_function) + + result = await asyncio.wait_for( + manager.run( + pool_id="pool", + worker_id=0, + func_import_path_or_key="subprocess_fn", + send_channel=False, + func_args=(), + func_kwargs={}, + ), + timeout=10.0, + ) + assert result.result == "subprocess_output" + +# %% +await test_async_subprocess_in_thread_pool(); + +# %% +#|export +@pytest.mark.asyncio +async def test_nested_async_in_thread_pool(): + """Test that nested async calls work on ThreadPool. + + Previously, nested run_until_complete calls were fragile. The shared + event loop provides natural await semantics. + """ + manager = ExecutionManager({ + "pool": (ThreadPool, {"num_workers": 1}), + }) + async with manager: + await manager.send_function("pool", 0, "nested", nested_async_function) + + result = await manager.run( + pool_id="pool", + worker_id=0, + func_import_path_or_key="nested", + send_channel=False, + func_args=(), + func_kwargs={}, + ) + assert result.result == "outer_inner_result" + +# %% +await test_nested_async_in_thread_pool(); + +# %% +#|export +@pytest.mark.asyncio +async def test_sync_function_unaffected_by_shared_loop(): + """Regression test: sync functions still work correctly on ThreadPool. + + Sync functions should run directly in the worker thread, not through + the shared event loop. + """ + import time + + def cpu_bound_sync(n: int) -> int: + """Sync function that does CPU work.""" + total = 0 + for i in range(n): + total += i + return total + + manager = ExecutionManager({ + "pool": (ThreadPool, {"num_workers": 2}), + }) + async with manager: + await manager.send_function_to_pool("pool", "cpu", cpu_bound_sync) + + # Run two sync functions concurrently on different workers + start = time.monotonic() + tasks = [ + asyncio.create_task( + manager.run( + pool_id="pool", + worker_id=i, + func_import_path_or_key="cpu", + send_channel=False, + func_args=(100_000,), + func_kwargs={}, + ) + ) + for i in range(2) + ] + results = await asyncio.gather(*tasks) + + # Both should complete with correct results + expected = sum(range(100_000)) + for r in results: + assert r.result == expected + +# %% +await test_sync_function_unaffected_by_shared_loop(); + +# %% +#|export +@pytest.mark.asyncio +async def test_asyncio_event_across_workers(): + """Test that asyncio primitives work across ThreadPool workers. + + Two async functions on different workers of the same pool share an + asyncio.Event. One sets it, the other waits on it. This verifies + cross-worker async coordination via the shared event loop. + """ + shared_event = asyncio.Event() + + async def setter(delay: float) -> str: + await asyncio.sleep(delay) + shared_event.set() + return "set" + + async def waiter() -> str: + await asyncio.wait_for(shared_event.wait(), timeout=5.0) + return "received" + + manager = ExecutionManager({ + "pool": (ThreadPool, {"num_workers": 2}), + }) + async with manager: + await manager.send_function("pool", 0, "setter", setter) + await manager.send_function("pool", 1, "waiter", waiter) + + # Start both concurrently + setter_task = asyncio.create_task( + manager.run( + pool_id="pool", + worker_id=0, + func_import_path_or_key="setter", + send_channel=False, + func_args=(0.1,), + func_kwargs={}, + ) + ) + waiter_task = asyncio.create_task( + manager.run( + pool_id="pool", + worker_id=1, + func_import_path_or_key="waiter", + send_channel=False, + func_args=(), + func_kwargs={}, + ) + ) + + setter_result, waiter_result = await asyncio.gather(setter_task, waiter_task) + assert setter_result.result == "set" + assert waiter_result.result == "received" + +# %% +await test_asyncio_event_across_workers(); + +# %% +#|export +@pytest.mark.asyncio +async def test_shared_loop_cleanup(): + """Test that the shared event loop is properly cleaned up on close.""" + import threading + + manager = ExecutionManager({ + "pool": (ThreadPool, {"num_workers": 1}), + }) + async with manager: + # The shared loop thread should be alive + assert manager._shared_loop is not None + assert manager._shared_loop_thread is not None + assert manager._shared_loop_thread.is_alive() + loop_thread = manager._shared_loop_thread + + # After close, the loop thread should have stopped + assert not loop_thread.is_alive() + assert manager._shared_loop is None + +# %% +await test_shared_loop_cleanup(); diff --git a/netrun/pts/tests/05_execution_manager/workers.pct.py b/netrun/pts/tests/05_execution_manager/workers.pct.py index 078e3c68..7793b8a2 100644 --- a/netrun/pts/tests/05_execution_manager/workers.pct.py +++ b/netrun/pts/tests/05_execution_manager/workers.pct.py @@ -89,3 +89,31 @@ def slow_printing_function(iterations: int, delay: float) -> str: print(f"Step {i}") time.sleep(delay) return "done" + + +async def async_subprocess_function() -> str: + """Async function that runs a subprocess via asyncio.create_subprocess_exec. + + This was hanging on per-thread event loops that lacked child watchers. + With the shared event loop, it should work correctly. + """ + proc = await asyncio.create_subprocess_exec( + sys.executable, "-c", "print('subprocess_output')", + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + stdout, _ = await proc.communicate() + return stdout.decode().strip() + + +async def nested_async_function() -> str: + """Async function with nested await chain. + + Tests that natural await semantics work (no nested run_until_complete). + """ + async def inner(): + await asyncio.sleep(0.01) + return "inner_result" + + result = await inner() + return f"outer_{result}" diff --git a/netrun/src/netrun/execution_manager.py b/netrun/src/netrun/execution_manager.py index 31690c4d..3cd31024 100644 --- a/netrun/src/netrun/execution_manager.py +++ b/netrun/src/netrun/execution_manager.py @@ -8,6 +8,7 @@ from ._iutils import get_timestamp_utc from datetime import datetime import asyncio +import threading from enum import Enum from dataclasses import dataclass import importlib @@ -48,18 +49,33 @@ def _func_runner( send_channel: bool, args: tuple, kwargs: dict, - event_loop: asyncio.AbstractEventLoop, + shared_loop: asyncio.AbstractEventLoop | None = None, ) -> Any: + """Run a function (sync or async) in a worker thread. + + When a func_preprocessor is used, it returns a sync wrapper that handles + async dispatch internally. When called directly without a preprocessor, + async functions are submitted to the shared event loop. + """ + if send_channel: + call_args = (channel, *args) + else: + call_args = args + if asyncio.iscoroutinefunction(func): - if send_channel: - return event_loop.run_until_complete(func(channel, *args, **kwargs)) + coro = func(*call_args, **kwargs) + if shared_loop is not None: + future = asyncio.run_coroutine_threadsafe(coro, shared_loop) + return future.result() else: - return event_loop.run_until_complete(func(*args, **kwargs)) + # Fallback: create a temporary event loop (for backward compat) + loop = asyncio.new_event_loop() + try: + return loop.run_until_complete(coro) + finally: + loop.close() else: - if send_channel: - return func(channel, *args, **kwargs) - else: - return func(*args, **kwargs) + return func(*call_args, **kwargs) # %% pts/netrun/05_execution_manager.pct.py 8 def _convert_to_str_if_not_serializable(obj: Any) -> tuple[bool, Any]: @@ -113,6 +129,7 @@ def _worker_func( worker_id, func_preprocessor: Callable | None = None, func_done_callback: Callable | None = None, + shared_loop: asyncio.AbstractEventLoop | None = None, ): """Worker function that handles execution manager protocol messages. @@ -120,89 +137,87 @@ def _worker_func( is_in_main_process: If True, results don't need to be serializable. channel: RPC channel for communication. worker_id: ID of this worker. - func_preprocessor: If provided, this is a function that preprocesses the function before it is executed. - func_done_callback: If provided, this is called after function execution with the same args/kwargs - that were passed to the function, plus the result. Signature: callback(channel, *args, **kwargs, result=result) - if send_channel was True, otherwise callback(*args, **kwargs, result=result). + func_preprocessor: If provided, preprocesses the function before execution. + Uses call_for_sync_worker() for sync dispatch with shared_loop for async. + func_done_callback: If provided, called after function execution. + shared_loop: Shared event loop for async function dispatch. Async user + functions are submitted to this loop via run_coroutine_threadsafe(). """ + # Set shared loop on preprocessor so its sync wrapper can dispatch async functions + if func_preprocessor is not None and shared_loop is not None: + func_preprocessor._shared_loop = shared_loop - event_loop = asyncio.new_event_loop() - asyncio.set_event_loop(event_loop) registered_functions: dict[str, Callable[..., Awaitable] | Callable[..., None]] = {} - try: - while True: - key, data = channel.recv() - # RUN - if key == ExecutionManagerProtocolKeys.RUN.value: - msg_id, func_import_path_or_key, run_id, send_channel, args, kwargs = data - timestamp_utc_started = None - started_sent = False - try: - if func_import_path_or_key in registered_functions: - func = registered_functions[func_import_path_or_key] - else: - module_path, func_name = func_import_path_or_key.rsplit(".", 1) - module = importlib.import_module(module_path) - func = getattr(module, func_name) - - if func_preprocessor is not None: - func = func_preprocessor(func) - - timestamp_utc_started = get_timestamp_utc() - channel.send(ExecutionManagerProtocolKeys.UP_RUN_STARTED.value, (msg_id, timestamp_utc_started)) - started_sent = True - res = _func_runner( - channel=channel, - func=func, - send_channel=send_channel, - args=args, - kwargs=kwargs, - event_loop=event_loop, - ) - - # Call done callback if provided - if func_done_callback is not None: - if send_channel: - func_done_callback(channel, *args, **kwargs, result=res) - else: - func_done_callback(*args, **kwargs, result=res) - - timestamp_utc_completed = get_timestamp_utc() - if is_in_main_process: - converted_to_str, _res = False, res + while True: + key, data = channel.recv() + # RUN + if key == ExecutionManagerProtocolKeys.RUN.value: + msg_id, func_import_path_or_key, run_id, send_channel, args, kwargs = data + timestamp_utc_started = None + started_sent = False + try: + if func_import_path_or_key in registered_functions: + func = registered_functions[func_import_path_or_key] + else: + module_path, func_name = func_import_path_or_key.rsplit(".", 1) + module = importlib.import_module(module_path) + func = getattr(module, func_name) + + if func_preprocessor is not None: + func = func_preprocessor.call_for_sync_worker(func) + + timestamp_utc_started = get_timestamp_utc() + channel.send(ExecutionManagerProtocolKeys.UP_RUN_STARTED.value, (msg_id, timestamp_utc_started)) + started_sent = True + res = _func_runner( + channel=channel, + func=func, + send_channel=send_channel, + args=args, + kwargs=kwargs, + shared_loop=shared_loop, + ) + + # Call done callback if provided + if func_done_callback is not None: + if send_channel: + func_done_callback(channel, *args, **kwargs, result=res) else: - converted_to_str, _res = _convert_to_str_if_not_serializable(res) - - channel.send(ExecutionManagerProtocolKeys.UP_RUN_RESPONSE.value, (msg_id, timestamp_utc_started, timestamp_utc_completed, converted_to_str, _res)) - except Exception as e: - # Send error response so the client doesn't hang forever - timestamp_utc_error = get_timestamp_utc() - if timestamp_utc_started is None: - timestamp_utc_started = timestamp_utc_error - try: - if not started_sent: - channel.send(ExecutionManagerProtocolKeys.UP_RUN_STARTED.value, (msg_id, timestamp_utc_started)) - channel.send(ExecutionManagerProtocolKeys.UP_RUN_RESPONSE.value, (msg_id, timestamp_utc_started, timestamp_utc_error, False, e)) - except Exception: - pass # Worker loop must survive - # SEND_FUNCTION - elif key == ExecutionManagerProtocolKeys.SEND_FUNCTION.value: - msg_id, func_key, func = data - registered_functions[func_key] = func - channel.send(ExecutionManagerProtocolKeys.UP_SEND_FUNCTION_RESPONSE.value, (msg_id,)) - else: - raise ValueError(f"Unknown execution manager protocol key: '{key}'.") - finally: - event_loop.close() - asyncio.set_event_loop(None) + func_done_callback(*args, **kwargs, result=res) + + timestamp_utc_completed = get_timestamp_utc() + if is_in_main_process: + converted_to_str, _res = False, res + else: + converted_to_str, _res = _convert_to_str_if_not_serializable(res) -def _thread_worker_func(channel, worker_id, func_preprocessor: Callable | None = None, func_done_callback: Callable | None = None): - return _worker_func(is_in_main_process=True, channel=channel, worker_id=worker_id, func_preprocessor=func_preprocessor, func_done_callback=func_done_callback) + channel.send(ExecutionManagerProtocolKeys.UP_RUN_RESPONSE.value, (msg_id, timestamp_utc_started, timestamp_utc_completed, converted_to_str, _res)) + except Exception as e: + # Send error response so the client doesn't hang forever + timestamp_utc_error = get_timestamp_utc() + if timestamp_utc_started is None: + timestamp_utc_started = timestamp_utc_error + try: + if not started_sent: + channel.send(ExecutionManagerProtocolKeys.UP_RUN_STARTED.value, (msg_id, timestamp_utc_started)) + channel.send(ExecutionManagerProtocolKeys.UP_RUN_RESPONSE.value, (msg_id, timestamp_utc_started, timestamp_utc_error, False, e)) + except Exception: + pass # Worker loop must survive + # SEND_FUNCTION + elif key == ExecutionManagerProtocolKeys.SEND_FUNCTION.value: + msg_id, func_key, func = data + registered_functions[func_key] = func + channel.send(ExecutionManagerProtocolKeys.UP_SEND_FUNCTION_RESPONSE.value, (msg_id,)) + else: + raise ValueError(f"Unknown execution manager protocol key: '{key}'.") + +def _thread_worker_func(channel, worker_id, func_preprocessor: Callable | None = None, func_done_callback: Callable | None = None, shared_loop: asyncio.AbstractEventLoop | None = None): + return _worker_func(is_in_main_process=True, channel=channel, worker_id=worker_id, func_preprocessor=func_preprocessor, func_done_callback=func_done_callback, shared_loop=shared_loop) # If the worker is in a multiprocess pool, then the result needs to be pickleable for it to be sent back without being converted as `str(result)`. -def _multiprocess_worker_func(channel, worker_id, func_preprocessor: Callable | None = None, func_done_callback: Callable | None = None): - return _worker_func(is_in_main_process=False, channel=channel, worker_id=worker_id, func_preprocessor=func_preprocessor, func_done_callback=func_done_callback) +def _multiprocess_worker_func(channel, worker_id, func_preprocessor: Callable | None = None, func_done_callback: Callable | None = None, shared_loop: asyncio.AbstractEventLoop | None = None): + return _worker_func(is_in_main_process=False, channel=channel, worker_id=worker_id, func_preprocessor=func_preprocessor, func_done_callback=func_done_callback, shared_loop=shared_loop) # %% pts/netrun/05_execution_manager.pct.py 12 async def _async_worker_func( @@ -303,6 +318,7 @@ def remote_execution_manager_worker( worker_id: int, func_preprocessor: Callable | None = None, func_done_callback: Callable | None = None, + shared_loop: asyncio.AbstractEventLoop | None = None, ) -> None: return _worker_func( is_in_main_process=False, @@ -310,6 +326,7 @@ def remote_execution_manager_worker( worker_id=worker_id, func_preprocessor=func_preprocessor, func_done_callback=func_done_callback, + shared_loop=shared_loop, ) # %% pts/netrun/05_execution_manager.pct.py 15 @@ -433,11 +450,29 @@ def __init__(self, pool_configs: dict[str, tuple[type[PoolType], dict[str, Any]] self._worker_jobs: dict[tuple[str, str], list[SubmittedJobInfo]] = {} # (pool_id, worker_id) -> list of SubmittedJobInfo self._worker_round_robin_lst: list[tuple[str, str]] = [] + # Shared event loop for async function dispatch in thread/multiprocess workers. + # Created in start(), cleaned up in close(). + self._shared_loop: asyncio.AbstractEventLoop | None = None + self._shared_loop_thread: threading.Thread | None = None + async def start(self) -> None: """Start all pools and initialize the execution manager.""" if self._started: raise RuntimeError("ExecutionManager is already started.") + # Create shared event loop on a dedicated daemon thread. + # Thread pool workers submit async coroutines to this loop via + # run_coroutine_threadsafe(). This replaces the broken per-thread + # event loops that lacked child watchers and couldn't share asyncio + # primitives across workers. + self._shared_loop = asyncio.new_event_loop() + self._shared_loop_thread = threading.Thread( + target=self._shared_loop.run_forever, + daemon=True, + name="netrun-shared-async-loop", + ) + self._shared_loop_thread.start() + for pool_id, (pool_type, pool_init_kwargs) in self._pool_configs.items(): if 'worker_fn' in pool_init_kwargs: raise ValueError("The 'worker_fn' argument should not be specified in the pool config.") @@ -447,14 +482,17 @@ async def start(self) -> None: func_done_callback = pool_kwargs.pop('func_done_callback', None) if pool_type == ThreadPool: - worker_fn = functools.partial(_thread_worker_func, func_preprocessor=func_preprocessor, func_done_callback=func_done_callback) + worker_fn = functools.partial(_thread_worker_func, func_preprocessor=func_preprocessor, func_done_callback=func_done_callback, shared_loop=self._shared_loop) self._pools[pool_id] = ThreadPool(**pool_kwargs, worker_fn=worker_fn) elif pool_type == MultiprocessPool: + # Multiprocess workers create their own process-local shared loop + # (event loops can't cross process boundaries) worker_fn = functools.partial(_multiprocess_worker_func, func_preprocessor=func_preprocessor, func_done_callback=func_done_callback) self._pools[pool_id] = MultiprocessPool(**pool_kwargs, worker_fn=worker_fn) elif pool_type == RemotePoolClient: self._pools[pool_id] = RemotePoolClient(**pool_kwargs) elif pool_type == SingleWorkerPool: + # SingleWorkerPool runs on the main event loop — no shared loop needed worker_fn = functools.partial(_async_worker_func, func_preprocessor=func_preprocessor, func_done_callback=func_done_callback) self._pools[pool_id] = SingleWorkerPool(**pool_kwargs, worker_fn=worker_fn) else: @@ -756,6 +794,15 @@ async def close(self): for pool in self._pools.values(): await pool.close() + # Stop the shared event loop + if self._shared_loop is not None: + self._shared_loop.call_soon_threadsafe(self._shared_loop.stop) + if self._shared_loop_thread is not None: + self._shared_loop_thread.join(timeout=5.0) + self._shared_loop.close() + self._shared_loop = None + self._shared_loop_thread = None + self._started = False # Propagate any errors from the recv tasks diff --git a/netrun/src/netrun/net/_net/_context.py b/netrun/src/netrun/net/_net/_context.py index 39984e8f..c59752dc 100644 --- a/netrun/src/netrun/net/_net/_context.py +++ b/netrun/src/netrun/net/_net/_context.py @@ -1121,6 +1121,12 @@ class NetFuncPreprocessor: Unlike the closure-based version, this class can be pickled for multiprocess pools. It resolves factory-based node functions lazily on each worker. + + Provides two wrapper modes: + - __call__: returns async wrapper (for SingleWorkerPool / async contexts) + - call_for_sync_worker: returns sync wrapper (for ThreadPool / MultiprocessPool) + Async user functions are dispatched to _shared_loop via run_coroutine_threadsafe. + Sync user functions run directly in the calling thread. """ def __init__( @@ -1135,6 +1141,8 @@ def __init__( self._node_configs = node_configs # Worker-local cache for resolved factory functions self._resolved_funcs: dict[str, Callable] = {} + # Shared event loop for async dispatch (set by worker on startup) + self._shared_loop: asyncio.AbstractEventLoop | None = None def _resolve_factory(self, node_name: str) -> Callable | None: """Resolve factory function on worker (lazy). @@ -1162,13 +1170,103 @@ def _resolve_factory(self, node_name: str) -> Callable | None: self._resolved_funcs[node_name] = exec_func return exec_func + def _setup_context( + self, + exec_node_func: Callable, + epoch_id: str, + node_name: str, + packets: dict[str, list[str]], + packet_values: dict[str, Any], + retry_count: int, + retry_timestamps: list[datetime] | None, + retry_exceptions: list[Exception] | None, + ) -> tuple[NodeExecutionContext, Callable]: + """Common setup for both sync and async wrappers. + + Returns: + (ctx, actual_func) — the execution context and resolved function. + """ + config = self._node_configs.get(node_name) + in_ports = {} + out_ports = {} + if config: + in_ports = { + name: PortConfig.model_validate(port_dict) + for name, port_dict in config.in_ports.items() + } + out_ports = { + name: PortConfig.model_validate(port_dict) + for name, port_dict in config.out_ports.items() + } + + # Create execution config for context (with essential fields) + exec_config = None + if config: + exec_config = NodeExecutionConfig( + capture_prints=config.capture_prints, + print_flush_interval=config.print_flush_interval, + print_buffer_max_size=config.print_buffer_max_size, + print_echo_stdout=config.print_echo_stdout, + retries=config.retries, + retry_wait=config.retry_wait, + timeout=config.timeout, + ) + + # Reconstruct NodeVariable objects from serialized dicts + _node_vars = None + if config and config.node_vars: + _node_vars = { + n: NodeVariable.model_validate(v) + for n, v in config.node_vars.items() + } + + # Get type checking enabled flag (default True if no config) + type_checking_enabled = config.type_checking_enabled if config else True + + ctx = NodeExecutionContext( + epoch_id=epoch_id, + node_name=node_name, + retry_count=retry_count, + retry_timestamps=retry_timestamps or [], + retry_exceptions=retry_exceptions or [], + _config=exec_config, + _input_packet_values=packet_values, + _in_ports=in_ports, + _out_ports=out_ports, + _node_vars=_node_vars, + _type_checking_enabled=type_checking_enabled, + ) + + # Determine the actual function to call + actual_func = exec_node_func + if isinstance(exec_node_func, _FactoryPlaceholder): + resolved = self._resolve_factory(node_name) + if resolved is None: + raise RuntimeError( + f"Failed to resolve factory function for node '{node_name}'" + ) + actual_func = resolved + + return ctx, actual_func + + @staticmethod + def _build_result( + ctx: NodeExecutionContext, + func_result: Any, + exception: Exception | None, + ) -> NodeExecutionResult: + """Build NodeExecutionResult from context and execution outcome.""" + result = ctx._get_execution_result() + result.func_result = func_result + result.exception = exception + return result + def __call__(self, exec_node_func: Callable) -> Callable: - """Transform exec_node_func -> wrapped function. + """Return an async wrapper for SingleWorkerPool (async context). - If exec_node_func is a _FactoryPlaceholder, resolves the actual function - from the factory on this worker. + The wrapper awaits async user functions and calls sync ones directly. """ - preprocessor_self = self # Capture for inner function + preprocessor_self = self async def wrapped( epoch_id: str, @@ -1179,90 +1277,74 @@ async def wrapped( retry_timestamps: list[datetime] | None = None, retry_exceptions: list[Exception] | None = None, ) -> NodeExecutionResult: - config = preprocessor_self._node_configs.get(node_name) - in_ports = {} - out_ports = {} - if config: - in_ports = { - name: PortConfig.model_validate(port_dict) - for name, port_dict in config.in_ports.items() - } - out_ports = { - name: PortConfig.model_validate(port_dict) - for name, port_dict in config.out_ports.items() - } + ctx, actual_func = preprocessor_self._setup_context( + exec_node_func, epoch_id, node_name, packets, packet_values, + retry_count, retry_timestamps, retry_exceptions, + ) - # Create execution config for context (with essential fields) - exec_config = None - if config: - exec_config = NodeExecutionConfig( - capture_prints=config.capture_prints, - print_flush_interval=config.print_flush_interval, - print_buffer_max_size=config.print_buffer_max_size, - print_echo_stdout=config.print_echo_stdout, - retries=config.retries, - retry_wait=config.retry_wait, - timeout=config.timeout, - ) + func_result = None + exception = None - # Reconstruct NodeVariable objects from serialized dicts - _node_vars = None - if config and config.node_vars: - _node_vars = { - n: NodeVariable.model_validate(v) - for n, v in config.node_vars.items() - } + try: + ctx._validate_input_packets(packets) + if asyncio.iscoroutinefunction(actual_func): + func_result = await actual_func(ctx, packets) + else: + func_result = actual_func(ctx, packets) + except EpochCancelled: + pass + except Exception as e: + exception = e - # Get type checking enabled flag (default True if no config) - type_checking_enabled = config.type_checking_enabled if config else True - - ctx = NodeExecutionContext( - epoch_id=epoch_id, - node_name=node_name, - retry_count=retry_count, - retry_timestamps=retry_timestamps or [], - retry_exceptions=retry_exceptions or [], - _config=exec_config, - _input_packet_values=packet_values, - _in_ports=in_ports, - _out_ports=out_ports, - _node_vars=_node_vars, - _type_checking_enabled=type_checking_enabled, - ) + return preprocessor_self._build_result(ctx, func_result, exception) + + return wrapped + + def call_for_sync_worker(self, exec_node_func: Callable) -> Callable: + """Return a sync wrapper for ThreadPool/MultiprocessPool (sync context). - # Determine the actual function to call - actual_func = exec_node_func - if isinstance(exec_node_func, _FactoryPlaceholder): - # Resolve factory function on this worker - resolved = preprocessor_self._resolve_factory(node_name) - if resolved is None: - raise RuntimeError( - f"Failed to resolve factory function for node '{node_name}'" - ) - actual_func = resolved + Sync user functions run directly in the calling worker thread. + Async user functions are dispatched to the shared event loop via + run_coroutine_threadsafe and the worker thread blocks on the result. + """ + preprocessor_self = self + + def wrapped( + epoch_id: str, + node_name: str, + packets: dict[str, list[str]], + packet_values: dict[str, Any], + retry_count: int = 0, + retry_timestamps: list[datetime] | None = None, + retry_exceptions: list[Exception] | None = None, + ) -> NodeExecutionResult: + ctx, actual_func = preprocessor_self._setup_context( + exec_node_func, epoch_id, node_name, packets, packet_values, + retry_count, retry_timestamps, retry_exceptions, + ) func_result = None exception = None try: - # Validate input packet types before executing the node function ctx._validate_input_packets(packets) if asyncio.iscoroutinefunction(actual_func): - func_result = await actual_func(ctx, packets) + if preprocessor_self._shared_loop is None: + raise RuntimeError( + f"Async node function '{node_name}' requires a shared event loop, " + "but none was set on the preprocessor. This is a netrun internal error." + ) + coro = actual_func(ctx, packets) + future = asyncio.run_coroutine_threadsafe(coro, preprocessor_self._shared_loop) + func_result = future.result() else: func_result = actual_func(ctx, packets) except EpochCancelled: - # Expected when ctx.cancel_epoch() is called pass except Exception as e: exception = e - # Get the execution result with all deferred actions - result = ctx._get_execution_result() - result.func_result = func_result - result.exception = exception - - return result + return preprocessor_self._build_result(ctx, func_result, exception) return wrapped diff --git a/netrun/src/netrun/pool/multiprocess.py b/netrun/src/netrun/pool/multiprocess.py index 5e2e1371..4316371e 100644 --- a/netrun/src/netrun/pool/multiprocess.py +++ b/netrun/src/netrun/pool/multiprocess.py @@ -183,6 +183,16 @@ def _subprocess_main_inner( # Create channel to parent parent_channel = SyncProcessChannel(parent_send_q, parent_recv_q) + # Create process-local shared event loop for async function dispatch. + # Each subprocess gets its own loop (event loops can't cross process boundaries). + shared_loop = asyncio.new_event_loop() + shared_loop_thread = threading.Thread( + target=shared_loop.run_forever, + daemon=True, + name=f"netrun-subprocess-{process_idx}-async-loop", + ) + shared_loop_thread.start() + # Create thread-safe queues for each worker # worker_queues[thread_idx] = (send_to_worker, recv_from_worker) worker_send_queues: list[queue.Queue] = [queue.Queue() for _ in range(num_threads)] @@ -195,6 +205,7 @@ def _subprocess_main_inner( t = threading.Thread( target=_thread_worker, args=(worker_fn, worker_send_queues[thread_idx], response_queue, worker_id), + kwargs={"shared_loop": shared_loop}, daemon=True, ) t.start() @@ -314,6 +325,11 @@ def output_flusher(): except Exception: pass + # Stop the process-local shared event loop + shared_loop.call_soon_threadsafe(shared_loop.stop) + shared_loop_thread.join(timeout=5.0) + shared_loop.close() + # Signal that shutdown is complete try: parent_channel.send(MP_UP_SHUTDOWN_COMPLETE, None) @@ -326,6 +342,7 @@ def _thread_worker( recv_queue: queue.Queue, response_queue: queue.Queue, worker_id: WorkerId, + shared_loop: asyncio.AbstractEventLoop | None = None, ): """Run worker function in a thread within subprocess.""" @@ -373,7 +390,20 @@ def is_closed(self) -> bool: channel = _WorkerChannel() try: - worker_fn(channel, worker_id) + # Only pass shared_loop if the worker function accepts it. + # ExecutionManager worker functions accept it; raw pool worker functions don't. + import inspect + _accepts_shared_loop = False + if shared_loop is not None: + try: + sig = inspect.signature(worker_fn) + _accepts_shared_loop = "shared_loop" in sig.parameters + except (ValueError, TypeError): + pass + if _accepts_shared_loop: + worker_fn(channel, worker_id, shared_loop=shared_loop) + else: + worker_fn(channel, worker_id) except ChannelClosed: pass except Exception as e: diff --git a/netrun/src/tests/execution_manager/test_execution_manager_thread.py b/netrun/src/tests/execution_manager/test_execution_manager_thread.py index 4f2734b9..8c3d541e 100644 --- a/netrun/src/tests/execution_manager/test_execution_manager_thread.py +++ b/netrun/src/tests/execution_manager/test_execution_manager_thread.py @@ -1,6 +1,6 @@ # AUTOGENERATED! DO NOT EDIT! File to edit: pts/tests/05_execution_manager/test_execution_manager_thread.pct.py -__all__ = ['test_allocation_with_specific_workers', 'test_async_function', 'test_concurrent_jobs', 'test_context_manager', 'test_create_execution_manager', 'test_create_multiple_pools', 'test_double_start_raises', 'test_empty_workers_raises', 'test_function_with_kwargs', 'test_get_num_workers', 'test_get_worker_jobs_empty', 'test_immediate_close', 'test_invalid_pool_type', 'test_job_result_timestamps', 'test_main_pool', 'test_multiple_pools', 'test_non_serializable_result_for_main_process', 'test_pool_ids', 'test_random_allocation', 'test_round_robin_allocation', 'test_send_function_and_run', 'test_send_function_to_pool', 'test_start_and_close', 'test_worker_exception_does_not_crash_loop', 'test_worker_jobs_cleanup_on_cancellation'] +__all__ = ['test_allocation_with_specific_workers', 'test_async_function', 'test_async_subprocess_in_thread_pool', 'test_asyncio_event_across_workers', 'test_concurrent_jobs', 'test_context_manager', 'test_create_execution_manager', 'test_create_multiple_pools', 'test_double_start_raises', 'test_empty_workers_raises', 'test_function_with_kwargs', 'test_get_num_workers', 'test_get_worker_jobs_empty', 'test_immediate_close', 'test_invalid_pool_type', 'test_job_result_timestamps', 'test_main_pool', 'test_multiple_pools', 'test_nested_async_in_thread_pool', 'test_non_serializable_result_for_main_process', 'test_pool_ids', 'test_random_allocation', 'test_round_robin_allocation', 'test_send_function_and_run', 'test_send_function_to_pool', 'test_shared_loop_cleanup', 'test_start_and_close', 'test_sync_function_unaffected_by_shared_loop', 'test_worker_exception_does_not_crash_loop', 'test_worker_jobs_cleanup_on_cancellation'] # %% pts/tests/05_execution_manager/test_execution_manager_thread.pct.py 2 import pytest @@ -24,6 +24,8 @@ function_returns_non_serializable, async_add, function_with_kwargs, + async_subprocess_function, + nested_async_function, ) # %% pts/tests/05_execution_manager/test_execution_manager_thread.pct.py 4 @@ -597,3 +599,173 @@ async def test_worker_exception_does_not_crash_loop(): func_kwargs={}, ) assert result.result == 3 + +# %% pts/tests/05_execution_manager/test_execution_manager_thread.pct.py 68 +@pytest.mark.asyncio +async def test_async_subprocess_in_thread_pool(): + """Test that async subprocess works on ThreadPool via shared event loop. + + Previously, per-thread event loops lacked child watchers, causing + asyncio.create_subprocess_exec to hang forever. The shared event loop + fixes this. + """ + manager = ExecutionManager({ + "pool": (ThreadPool, {"num_workers": 1}), + }) + async with manager: + await manager.send_function("pool", 0, "subprocess_fn", async_subprocess_function) + + result = await asyncio.wait_for( + manager.run( + pool_id="pool", + worker_id=0, + func_import_path_or_key="subprocess_fn", + send_channel=False, + func_args=(), + func_kwargs={}, + ), + timeout=10.0, + ) + assert result.result == "subprocess_output" + +# %% pts/tests/05_execution_manager/test_execution_manager_thread.pct.py 70 +@pytest.mark.asyncio +async def test_nested_async_in_thread_pool(): + """Test that nested async calls work on ThreadPool. + + Previously, nested run_until_complete calls were fragile. The shared + event loop provides natural await semantics. + """ + manager = ExecutionManager({ + "pool": (ThreadPool, {"num_workers": 1}), + }) + async with manager: + await manager.send_function("pool", 0, "nested", nested_async_function) + + result = await manager.run( + pool_id="pool", + worker_id=0, + func_import_path_or_key="nested", + send_channel=False, + func_args=(), + func_kwargs={}, + ) + assert result.result == "outer_inner_result" + +# %% pts/tests/05_execution_manager/test_execution_manager_thread.pct.py 72 +@pytest.mark.asyncio +async def test_sync_function_unaffected_by_shared_loop(): + """Regression test: sync functions still work correctly on ThreadPool. + + Sync functions should run directly in the worker thread, not through + the shared event loop. + """ + import time + + def cpu_bound_sync(n: int) -> int: + """Sync function that does CPU work.""" + total = 0 + for i in range(n): + total += i + return total + + manager = ExecutionManager({ + "pool": (ThreadPool, {"num_workers": 2}), + }) + async with manager: + await manager.send_function_to_pool("pool", "cpu", cpu_bound_sync) + + # Run two sync functions concurrently on different workers + start = time.monotonic() + tasks = [ + asyncio.create_task( + manager.run( + pool_id="pool", + worker_id=i, + func_import_path_or_key="cpu", + send_channel=False, + func_args=(100_000,), + func_kwargs={}, + ) + ) + for i in range(2) + ] + results = await asyncio.gather(*tasks) + + # Both should complete with correct results + expected = sum(range(100_000)) + for r in results: + assert r.result == expected + +# %% pts/tests/05_execution_manager/test_execution_manager_thread.pct.py 74 +@pytest.mark.asyncio +async def test_asyncio_event_across_workers(): + """Test that asyncio primitives work across ThreadPool workers. + + Two async functions on different workers of the same pool share an + asyncio.Event. One sets it, the other waits on it. This verifies + cross-worker async coordination via the shared event loop. + """ + shared_event = asyncio.Event() + + async def setter(delay: float) -> str: + await asyncio.sleep(delay) + shared_event.set() + return "set" + + async def waiter() -> str: + await asyncio.wait_for(shared_event.wait(), timeout=5.0) + return "received" + + manager = ExecutionManager({ + "pool": (ThreadPool, {"num_workers": 2}), + }) + async with manager: + await manager.send_function("pool", 0, "setter", setter) + await manager.send_function("pool", 1, "waiter", waiter) + + # Start both concurrently + setter_task = asyncio.create_task( + manager.run( + pool_id="pool", + worker_id=0, + func_import_path_or_key="setter", + send_channel=False, + func_args=(0.1,), + func_kwargs={}, + ) + ) + waiter_task = asyncio.create_task( + manager.run( + pool_id="pool", + worker_id=1, + func_import_path_or_key="waiter", + send_channel=False, + func_args=(), + func_kwargs={}, + ) + ) + + setter_result, waiter_result = await asyncio.gather(setter_task, waiter_task) + assert setter_result.result == "set" + assert waiter_result.result == "received" + +# %% pts/tests/05_execution_manager/test_execution_manager_thread.pct.py 76 +@pytest.mark.asyncio +async def test_shared_loop_cleanup(): + """Test that the shared event loop is properly cleaned up on close.""" + import threading + + manager = ExecutionManager({ + "pool": (ThreadPool, {"num_workers": 1}), + }) + async with manager: + # The shared loop thread should be alive + assert manager._shared_loop is not None + assert manager._shared_loop_thread is not None + assert manager._shared_loop_thread.is_alive() + loop_thread = manager._shared_loop_thread + + # After close, the loop thread should have stopped + assert not loop_thread.is_alive() + assert manager._shared_loop is None diff --git a/netrun/src/tests/execution_manager/workers.py b/netrun/src/tests/execution_manager/workers.py index 85d0d741..ecb51f72 100644 --- a/netrun/src/tests/execution_manager/workers.py +++ b/netrun/src/tests/execution_manager/workers.py @@ -1,6 +1,6 @@ # AUTOGENERATED! DO NOT EDIT! File to edit: pts/tests/05_execution_manager/workers.pct.py -__all__ = ['add_numbers', 'async_add', 'function_returns_non_serializable', 'function_with_error', 'function_with_kwargs', 'function_with_multiple_prints', 'function_with_print', 'mp_stdout_function', 'multiply_numbers', 'slow_function', 'slow_printing_function'] +__all__ = ['add_numbers', 'async_add', 'async_subprocess_function', 'function_returns_non_serializable', 'function_with_error', 'function_with_kwargs', 'function_with_multiple_prints', 'function_with_print', 'mp_stdout_function', 'multiply_numbers', 'nested_async_function', 'slow_function', 'slow_printing_function'] # %% pts/tests/05_execution_manager/workers.pct.py 2 import asyncio @@ -75,3 +75,31 @@ def slow_printing_function(iterations: int, delay: float) -> str: print(f"Step {i}") time.sleep(delay) return "done" + + +async def async_subprocess_function() -> str: + """Async function that runs a subprocess via asyncio.create_subprocess_exec. + + This was hanging on per-thread event loops that lacked child watchers. + With the shared event loop, it should work correctly. + """ + proc = await asyncio.create_subprocess_exec( + sys.executable, "-c", "print('subprocess_output')", + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + stdout, _ = await proc.communicate() + return stdout.decode().strip() + + +async def nested_async_function() -> str: + """Async function with nested await chain. + + Tests that natural await semantics work (no nested run_until_complete). + """ + async def inner(): + await asyncio.sleep(0.01) + return "inner_result" + + result = await inner() + return f"outer_{result}" From 2c436e8e19e0098d306749b5ded563b1a10098ad Mon Sep 17 00:00:00 2001 From: Lukas Kikuchi Date: Sat, 25 Apr 2026 16:53:00 +0100 Subject: [PATCH 03/15] feat(netrun): add ctx.state, depends_on, and resources scheduling Phase 2 of the major refactor. Three additive API features that address production pain without changing existing behavior. ctx.state: - Mutable dict on NodeExecutionContext preserved across retries - Same dict instance reused across retry attempts for the same epoch - Cleared on final success or permanent failure - Lets retry-heavy nodes cache expensive state (e.g., 10GB DataFrames) depends_on: - New field on NodeExecutionConfig: depends_on: list[str] - Node epoch can't start until all named dependencies have completed at least one epoch successfully - Enforced in _filter_startable_epochs (scheduler-level gating) - Replaces manual control-edge wiring for ordering constraints resources: - Named semaphores for bounded concurrency and mutual exclusion - Net-level: NetConfig.resources defines capacity (e.g., {"heavy_mem": 1}) - Node-level: NodeExecutionConfig.resources declares requirements - Acquired when epoch starts, released in finally block - Replaces the heavy-pool workaround (single-worker pool for mutex) Co-Authored-By: Claude Opus 4.6 (1M context) --- .../netrun/06_net/00_config/01_nodes.pct.py | 4 + .../06_net/00_config/03_net_config.pct.py | 2 + .../netrun/06_net/01_net/00_context.pct.py | 13 +- netrun/pts/netrun/06_net/01_net/02_net.pct.py | 57 +++ netrun/pts/tests/06_net/test_net.pct.py | 353 ++++++++++++++++++ netrun/src/netrun/net/_net/_context.py | 13 +- netrun/src/netrun/net/_net/_net.py | 57 +++ netrun/src/netrun/net/config/_net_config.py | 2 + netrun/src/netrun/net/config/_nodes.py | 4 + netrun/src/tests/net/test_net.py | 326 +++++++++++++++- 10 files changed, 826 insertions(+), 5 deletions(-) diff --git a/netrun/pts/netrun/06_net/00_config/01_nodes.pct.py b/netrun/pts/netrun/06_net/00_config/01_nodes.pct.py index 2d5121ed..6872d5cb 100644 --- a/netrun/pts/netrun/06_net/00_config/01_nodes.pct.py +++ b/netrun/pts/netrun/06_net/00_config/01_nodes.pct.py @@ -267,6 +267,10 @@ class NodeExecutionConfig(VarResolvableModel): rate_limit_per_second: float | VarRef | None = Field(default=None, description="Maximum epoch triggers per second.") + depends_on: list[str] | None = Field(default=None, description="Node names that must have completed at least one epoch before this node can start. Provides directional ordering without explicit control edges.") + + resources: dict[str, int] | None = Field(default=None, description="Resource requirements as {resource_name: slots_needed}. Resources must be defined in NetConfig.resources with their capacity. Node epoch starts only when all required resource slots are available.") + defer_net_actions: bool | VarRef | None = Field(default=None, description="Defer net action notifications until epoch completes successfully. Required if retries enabled.") retries: int | VarRef | None = Field(default=None, description="Number of retry attempts on failure. None inherits from NetConfig.") diff --git a/netrun/pts/netrun/06_net/00_config/03_net_config.pct.py b/netrun/pts/netrun/06_net/00_config/03_net_config.pct.py index 4380cbc0..24fc64ff 100644 --- a/netrun/pts/netrun/06_net/00_config/03_net_config.pct.py +++ b/netrun/pts/netrun/06_net/00_config/03_net_config.pct.py @@ -318,6 +318,8 @@ def from_file( pools: dict[str, PoolConfig] | None = Field(default=None, description="Pool configurations. None generates a default main pool on resolve().") graph: GraphConfig + resources: dict[str, int] | None = Field(default=None, description="Named resource capacities as {resource_name: max_slots}. Nodes declare resource requirements in execution_config.resources. The scheduler ensures usage never exceeds capacity.") + extra: dict[str, Any] = Field(default_factory=dict, description="Arbitrary extra data (descriptions, version info, tool-specific data).") default_pool_allocation_method: RunAllocationMethod | VarRef = Field(default=RunAllocationMethod.ROUND_ROBIN, description="Default worker allocation method for nodes with multiple pools.") diff --git a/netrun/pts/netrun/06_net/01_net/00_context.pct.py b/netrun/pts/netrun/06_net/01_net/00_context.pct.py index ce515516..94eecb7e 100644 --- a/netrun/pts/netrun/06_net/01_net/00_context.pct.py +++ b/netrun/pts/netrun/06_net/01_net/00_context.pct.py @@ -432,6 +432,11 @@ class NodeExecutionContext: retry_timestamps: list[datetime] = field(default_factory=list) retry_exceptions: list[Exception] = field(default_factory=list) + # Mutable state preserved across retries for the same epoch. + # Empty dict on first attempt; same dict instance reused on retries. + # Cleared when the epoch succeeds or permanently fails. + state: dict[str, Any] = field(default_factory=dict) + # Internal (not for user access) _config: NodeExecutionConfig = field(repr=False, default=None) _print_buffer: list[tuple[datetime, str]] = field(default_factory=list, repr=False) @@ -1261,6 +1266,7 @@ def _setup_context( retry_count: int, retry_timestamps: list[datetime] | None, retry_exceptions: list[Exception] | None, + state: dict[str, Any] | None = None, ) -> tuple[NodeExecutionContext, Callable]: """Common setup for both sync and async wrappers. @@ -1310,6 +1316,7 @@ def _setup_context( retry_count=retry_count, retry_timestamps=retry_timestamps or [], retry_exceptions=retry_exceptions or [], + state=state if state is not None else {}, _config=exec_config, _input_packet_values=packet_values, _in_ports=in_ports, @@ -1357,10 +1364,11 @@ async def wrapped( retry_count: int = 0, retry_timestamps: list[datetime] | None = None, retry_exceptions: list[Exception] | None = None, + state: dict[str, Any] | None = None, ) -> NodeExecutionResult: ctx, actual_func = preprocessor_self._setup_context( exec_node_func, epoch_id, node_name, packets, packet_values, - retry_count, retry_timestamps, retry_exceptions, + retry_count, retry_timestamps, retry_exceptions, state=state, ) func_result = None @@ -1398,10 +1406,11 @@ def wrapped( retry_count: int = 0, retry_timestamps: list[datetime] | None = None, retry_exceptions: list[Exception] | None = None, + state: dict[str, Any] | None = None, ) -> NodeExecutionResult: ctx, actual_func = preprocessor_self._setup_context( exec_node_func, epoch_id, node_name, packets, packet_values, - retry_count, retry_timestamps, retry_exceptions, + retry_count, retry_timestamps, retry_exceptions, state=state, ) func_result = None diff --git a/netrun/pts/netrun/06_net/01_net/02_net.pct.py b/netrun/pts/netrun/06_net/01_net/02_net.pct.py index 59c176f8..7f993795 100644 --- a/netrun/pts/netrun/06_net/01_net/02_net.pct.py +++ b/netrun/pts/netrun/06_net/01_net/02_net.pct.py @@ -434,6 +434,14 @@ def __init__(self, config: NetConfig, run_init_nodes: bool = True): # Running epoch tracking self._running_epochs: set[str] = set() + # Cross-retry state dicts (epoch_id -> mutable dict accessible via ctx.state) + self._epoch_states: dict[str, dict[str, Any]] = {} + + # Scheduling constraint tracking + self._node_has_completed: set[str] = set() # Nodes that have completed at least one epoch + self._resource_usage: dict[str, int] = {} # Current resource slot usage per resource name + self._resource_capacities: dict[str, int] = dict(self._config_resolved.resources) if self._config_resolved.resources else {} + # Epoch state (persists after epoch finishes in netsim) self._epochs: dict[str, _EpochState] = {} # epoch_id -> _EpochState @@ -1886,6 +1894,8 @@ def _filter_startable_epochs( allowed = [] # Track how many new epochs we're allowing per node (within this batch) new_per_node: dict[str, int] = {} + # Track resource slots claimed by newly-allowed epochs (within this batch) + new_resource_usage: dict[str, int] = {} for epoch_id in epoch_ids: epoch = self._netsim.get_epoch(epoch_id) @@ -1912,7 +1922,36 @@ def _filter_startable_epochs( if current >= max_parallel: continue # Skip — at the limit + # Check depends_on: all dependency nodes must have completed at least one epoch + if config and config.depends_on: + if not all(dep in self._node_has_completed for dep in config.depends_on): + continue # Skip — dependency not yet satisfied + + # Check resources: all required resource slots must be available + if config and config.resources and self._resource_capacities: + can_acquire = True + for res_name, needed in config.resources.items(): + capacity = self._resource_capacities.get(res_name) + if capacity is None: + raise ValueError( + f"Node '{node_name}' requires resource '{res_name}' " + f"which is not defined in NetConfig.resources" + ) + current_usage = ( + self._resource_usage.get(res_name, 0) + + new_resource_usage.get(res_name, 0) + ) + if current_usage + needed > capacity: + can_acquire = False + break + if not can_acquire: + continue # Skip — resource limit reached + new_per_node[node_name] = new_per_node.get(node_name, 0) + 1 + # Track resource usage within this batch + if config and config.resources: + for res_name, needed in config.resources.items(): + new_resource_usage[res_name] = new_resource_usage.get(res_name, 0) + needed allowed.append(epoch_id) return allowed @@ -1991,6 +2030,8 @@ async def _finish_epoch_lifecycle(self, epoch_id: str, node_name: str, *, retry_ self._do_action(netrun_sim.NetAction.finish_epoch(epoch_id), epoch_id=epoch_id, detail={"node_name": node_name}) self._epochs[epoch_id].ended_at = get_timestamp_utc() self._epochs[epoch_id].state = netrun_sim.EpochState.Finished + self._epoch_states.pop(epoch_id, None) # Clear cross-retry state on success + self._node_has_completed.add(node_name) # Mark node as having completed (for depends_on) await self._fire_epoch_end(node_name, epoch_id, retry_count=retry_count) def _consume_epoch_inputs(self, epoch_id: str, node_name: str, packets: dict[str, list[str]], consumed_ports: set[str] | list[str]) -> None: @@ -2278,6 +2319,12 @@ async def _execute_epoch(self, epoch_id: str) -> NodeExecutionResult | None: # Transition epoch to Running self._running_epochs.add(epoch_id) + + # Acquire resources for this epoch + if config.resources: + for res_name, needed in config.resources.items(): + self._resource_usage[res_name] = self._resource_usage.get(res_name, 0) + needed + await self._start_epoch_lifecycle(epoch_id, node_name) try: @@ -2310,6 +2357,10 @@ async def _execute_epoch(self, epoch_id: str) -> NodeExecutionResult | None: raise finally: self._running_epochs.discard(epoch_id) + # Release resources + if config.resources: + for res_name, needed in config.resources.items(): + self._resource_usage[res_name] = self._resource_usage.get(res_name, 0) - needed async def _execute_epoch_with_retry( self, @@ -2350,6 +2401,10 @@ async def _execute_epoch_with_retry( # Get func key for this node func_key = self._get_func_key(node_name) + # Initialize cross-retry state on first attempt + if retry_count == 0: + self._epoch_states[epoch_id] = {} + # Dispatch to worker (with optional timeout) try: coro = self._execution_manager.run_allocate( @@ -2362,6 +2417,7 @@ async def _execute_epoch_with_retry( "retry_count": retry_count, "retry_timestamps": retry_timestamps, "retry_exceptions": retry_exceptions, + "state": self._epoch_states.get(epoch_id, {}), }, ) effective_timeout = resolve_effective_exec_field("timeout", config, self._config_resolved) @@ -2986,6 +3042,7 @@ async def _handle_epoch_failure( record.was_cancelled = True record.ended_at = get_timestamp_utc() record.destroyed_packets = list(response.destroyed_packets) + self._epoch_states.pop(epoch_id, None) # Clear cross-retry state on permanent failure await self._fire_epoch_end(node_name, epoch_id, error=error, retry_count=retry_count) # Store in dead letter queue diff --git a/netrun/pts/tests/06_net/test_net.pct.py b/netrun/pts/tests/06_net/test_net.pct.py index a553e025..59c8db30 100644 --- a/netrun/pts/tests/06_net/test_net.pct.py +++ b/netrun/pts/tests/06_net/test_net.pct.py @@ -7153,3 +7153,356 @@ def failing_then_succeeds(ctx, packets): # %% asyncio.get_event_loop().run_until_complete(test_retry_calls_gc_collect()) + +# %% [markdown] +# ## ctx.state Tests + +# %% +#|export +@pytest.mark.asyncio +async def test_ctx_state_persists_across_retries(): + """Test that ctx.state is preserved across retry attempts.""" + call_log = [] + + def stateful_node(ctx, packets): + for port_name, pkt_ids in packets.items(): + for pid in pkt_ids: + ctx.consume_packet(pid) + + if "initialized" not in ctx.state: + ctx.state["initialized"] = True + ctx.state["counter"] = 0 + + ctx.state["counter"] += 1 + call_log.append({"retry": ctx.retry_count, "counter": ctx.state["counter"]}) + + if ctx.retry_count < 2: + raise ValueError(f"Fail on retry {ctx.retry_count}") + + graph_config = GraphConfig( + nodes=[ + NodeConfig( + name="Stateful", + in_ports={"in": PortConfig()}, + in_salvo_conditions={ + "default": SalvoConditionConfig( + max_salvos=MaxSalvosFiniteConfig(max=1), + ports={"in": PacketCountAllConfig()}, + term=SalvoConditionTermPortConfig( + port_name="in", + state=PortStateNonEmptyConfig(), + ), + ), + }, + execution_config=NodeExecutionConfig( + node_name="Stateful", + pools=["main"], + exec_node_func=stateful_node, + retries=3, + retry_wait=0.0, + ), + ), + ], + edges=[], + ) + config = NetConfig( + pools={"main": PoolConfig(spec=MainPoolConfig())}, + graph=graph_config, + ) + + async with Net(config) as net: + net.inject_data("Stateful", "in", [1]) + await net.run_until_blocked() + + # State should have persisted — counter incremented on each attempt + assert len(call_log) == 3 + assert call_log[0]["counter"] == 1 # First attempt + assert call_log[1]["counter"] == 2 # Second attempt (same dict) + assert call_log[2]["counter"] == 3 # Third attempt (same dict) + +# %% +asyncio.get_event_loop().run_until_complete(test_ctx_state_persists_across_retries()) + +# %% +#|export +@pytest.mark.asyncio +async def test_ctx_state_empty_by_default(): + """Test that ctx.state is an empty dict when not used.""" + observed_state = [None] + + def check_state(ctx, packets): + for port_name, pkt_ids in packets.items(): + for pid in pkt_ids: + ctx.consume_packet(pid) + observed_state[0] = dict(ctx.state) + + graph_config = GraphConfig( + nodes=[ + NodeConfig( + name="CheckState", + in_ports={"in": PortConfig()}, + in_salvo_conditions={ + "default": SalvoConditionConfig( + max_salvos=MaxSalvosFiniteConfig(max=1), + ports={"in": PacketCountAllConfig()}, + term=SalvoConditionTermPortConfig( + port_name="in", + state=PortStateNonEmptyConfig(), + ), + ), + }, + execution_config=NodeExecutionConfig( + node_name="CheckState", + pools=["main"], + exec_node_func=check_state, + ), + ), + ], + edges=[], + ) + config = NetConfig( + pools={"main": PoolConfig(spec=MainPoolConfig())}, + graph=graph_config, + ) + + async with Net(config) as net: + net.inject_data("CheckState", "in", [1]) + await net.run_until_blocked() + + assert observed_state[0] == {} + +# %% +asyncio.get_event_loop().run_until_complete(test_ctx_state_empty_by_default()) + +# %% [markdown] +# ## depends_on Tests + +# %% +#|export +@pytest.mark.asyncio +async def test_depends_on_blocks_until_dependency_completes(): + """Test that depends_on prevents a node from starting until its dependency completes.""" + execution_order = [] + + def node_a(ctx, packets): + for port_name, pkt_ids in packets.items(): + for pid in pkt_ids: + ctx.consume_packet(pid) + execution_order.append("A") + + def node_b(ctx, packets): + for port_name, pkt_ids in packets.items(): + for pid in pkt_ids: + ctx.consume_packet(pid) + execution_order.append("B") + + graph_config = GraphConfig( + nodes=[ + NodeConfig( + name="A", + in_ports={"in": PortConfig()}, + in_salvo_conditions={ + "default": SalvoConditionConfig( + max_salvos=MaxSalvosFiniteConfig(max=1), + ports={"in": PacketCountAllConfig()}, + term=SalvoConditionTermPortConfig( + port_name="in", + state=PortStateNonEmptyConfig(), + ), + ), + }, + execution_config=NodeExecutionConfig( + node_name="A", + pools=["main"], + exec_node_func=node_a, + ), + ), + NodeConfig( + name="B", + in_ports={"in": PortConfig()}, + in_salvo_conditions={ + "default": SalvoConditionConfig( + max_salvos=MaxSalvosFiniteConfig(max=1), + ports={"in": PacketCountAllConfig()}, + term=SalvoConditionTermPortConfig( + port_name="in", + state=PortStateNonEmptyConfig(), + ), + ), + }, + execution_config=NodeExecutionConfig( + node_name="B", + pools=["main"], + exec_node_func=node_b, + depends_on=["A"], + ), + ), + ], + edges=[], + ) + config = NetConfig( + pools={"main": PoolConfig(spec=MainPoolConfig())}, + graph=graph_config, + ) + + async with Net(config) as net: + # Inject data for both nodes + net.inject_data("A", "in", [1]) + net.inject_data("B", "in", [2]) + await net.run_until_blocked() + + # A must execute before B (B depends_on A) + assert execution_order == ["A", "B"] + +# %% +asyncio.get_event_loop().run_until_complete(test_depends_on_blocks_until_dependency_completes()) + +# %% [markdown] +# ## resources Tests + +# %% +#|export +@pytest.mark.asyncio +async def test_resources_mutual_exclusion(): + """Test that two nodes with the same 1-slot resource never run concurrently.""" + import time as _time + + timestamps = {} + + async def heavy_a(ctx, packets): + for port_name, pkt_ids in packets.items(): + for pid in pkt_ids: + ctx.consume_packet(pid) + timestamps["a_start"] = _time.monotonic() + await asyncio.sleep(0.1) + timestamps["a_end"] = _time.monotonic() + + async def heavy_b(ctx, packets): + for port_name, pkt_ids in packets.items(): + for pid in pkt_ids: + ctx.consume_packet(pid) + timestamps["b_start"] = _time.monotonic() + await asyncio.sleep(0.1) + timestamps["b_end"] = _time.monotonic() + + graph_config = GraphConfig( + nodes=[ + NodeConfig( + name="HeavyA", + in_ports={"in": PortConfig()}, + in_salvo_conditions={ + "default": SalvoConditionConfig( + max_salvos=MaxSalvosFiniteConfig(max=1), + ports={"in": PacketCountAllConfig()}, + term=SalvoConditionTermPortConfig( + port_name="in", + state=PortStateNonEmptyConfig(), + ), + ), + }, + execution_config=NodeExecutionConfig( + node_name="HeavyA", + pools=["main"], + exec_node_func=heavy_a, + resources={"heavy_mem": 1}, + ), + ), + NodeConfig( + name="HeavyB", + in_ports={"in": PortConfig()}, + in_salvo_conditions={ + "default": SalvoConditionConfig( + max_salvos=MaxSalvosFiniteConfig(max=1), + ports={"in": PacketCountAllConfig()}, + term=SalvoConditionTermPortConfig( + port_name="in", + state=PortStateNonEmptyConfig(), + ), + ), + }, + execution_config=NodeExecutionConfig( + node_name="HeavyB", + pools=["main"], + exec_node_func=heavy_b, + resources={"heavy_mem": 1}, + ), + ), + ], + edges=[], + ) + config = NetConfig( + pools={"main": PoolConfig(spec=MainPoolConfig())}, + resources={"heavy_mem": 1}, + graph=graph_config, + ) + + async with Net(config) as net: + net.inject_data("HeavyA", "in", [1]) + net.inject_data("HeavyB", "in", [2]) + await net.run_until_blocked() + + # Both should have completed + assert "a_end" in timestamps and "b_end" in timestamps + + # They must NOT have overlapped (mutual exclusion via 1-slot resource) + # One must have finished before the other started + a_before_b = timestamps["a_end"] <= timestamps["b_start"] + b_before_a = timestamps["b_end"] <= timestamps["a_start"] + assert a_before_b or b_before_a, ( + f"HeavyA and HeavyB overlapped: A=[{timestamps['a_start']:.3f}, {timestamps['a_end']:.3f}], " + f"B=[{timestamps['b_start']:.3f}, {timestamps['b_end']:.3f}]" + ) + +# %% +asyncio.get_event_loop().run_until_complete(test_resources_mutual_exclusion()) + +# %% +#|export +@pytest.mark.asyncio +async def test_resources_none_is_noop(): + """Test that nodes without resources work as before.""" + executed = [False] + + def simple_node(ctx, packets): + for port_name, pkt_ids in packets.items(): + for pid in pkt_ids: + ctx.consume_packet(pid) + executed[0] = True + + graph_config = GraphConfig( + nodes=[ + NodeConfig( + name="Simple", + in_ports={"in": PortConfig()}, + in_salvo_conditions={ + "default": SalvoConditionConfig( + max_salvos=MaxSalvosFiniteConfig(max=1), + ports={"in": PacketCountAllConfig()}, + term=SalvoConditionTermPortConfig( + port_name="in", + state=PortStateNonEmptyConfig(), + ), + ), + }, + execution_config=NodeExecutionConfig( + node_name="Simple", + pools=["main"], + exec_node_func=simple_node, + ), + ), + ], + edges=[], + ) + config = NetConfig( + pools={"main": PoolConfig(spec=MainPoolConfig())}, + graph=graph_config, + ) + + async with Net(config) as net: + net.inject_data("Simple", "in", [1]) + await net.run_until_blocked() + + assert executed[0] is True + +# %% +asyncio.get_event_loop().run_until_complete(test_resources_none_is_noop()) diff --git a/netrun/src/netrun/net/_net/_context.py b/netrun/src/netrun/net/_net/_context.py index c59752dc..1257ff22 100644 --- a/netrun/src/netrun/net/_net/_context.py +++ b/netrun/src/netrun/net/_net/_context.py @@ -389,6 +389,11 @@ class NodeExecutionContext: retry_timestamps: list[datetime] = field(default_factory=list) retry_exceptions: list[Exception] = field(default_factory=list) + # Mutable state preserved across retries for the same epoch. + # Empty dict on first attempt; same dict instance reused on retries. + # Cleared when the epoch succeeds or permanently fails. + state: dict[str, Any] = field(default_factory=dict) + # Internal (not for user access) _config: NodeExecutionConfig = field(repr=False, default=None) _print_buffer: list[tuple[datetime, str]] = field(default_factory=list, repr=False) @@ -1180,6 +1185,7 @@ def _setup_context( retry_count: int, retry_timestamps: list[datetime] | None, retry_exceptions: list[Exception] | None, + state: dict[str, Any] | None = None, ) -> tuple[NodeExecutionContext, Callable]: """Common setup for both sync and async wrappers. @@ -1229,6 +1235,7 @@ def _setup_context( retry_count=retry_count, retry_timestamps=retry_timestamps or [], retry_exceptions=retry_exceptions or [], + state=state if state is not None else {}, _config=exec_config, _input_packet_values=packet_values, _in_ports=in_ports, @@ -1276,10 +1283,11 @@ async def wrapped( retry_count: int = 0, retry_timestamps: list[datetime] | None = None, retry_exceptions: list[Exception] | None = None, + state: dict[str, Any] | None = None, ) -> NodeExecutionResult: ctx, actual_func = preprocessor_self._setup_context( exec_node_func, epoch_id, node_name, packets, packet_values, - retry_count, retry_timestamps, retry_exceptions, + retry_count, retry_timestamps, retry_exceptions, state=state, ) func_result = None @@ -1317,10 +1325,11 @@ def wrapped( retry_count: int = 0, retry_timestamps: list[datetime] | None = None, retry_exceptions: list[Exception] | None = None, + state: dict[str, Any] | None = None, ) -> NodeExecutionResult: ctx, actual_func = preprocessor_self._setup_context( exec_node_func, epoch_id, node_name, packets, packet_values, - retry_count, retry_timestamps, retry_exceptions, + retry_count, retry_timestamps, retry_exceptions, state=state, ) func_result = None diff --git a/netrun/src/netrun/net/_net/_net.py b/netrun/src/netrun/net/_net/_net.py index 26856802..62c26eb7 100644 --- a/netrun/src/netrun/net/_net/_net.py +++ b/netrun/src/netrun/net/_net/_net.py @@ -408,6 +408,14 @@ def __init__(self, config: NetConfig, run_init_nodes: bool = True): # Running epoch tracking self._running_epochs: set[str] = set() + # Cross-retry state dicts (epoch_id -> mutable dict accessible via ctx.state) + self._epoch_states: dict[str, dict[str, Any]] = {} + + # Scheduling constraint tracking + self._node_has_completed: set[str] = set() # Nodes that have completed at least one epoch + self._resource_usage: dict[str, int] = {} # Current resource slot usage per resource name + self._resource_capacities: dict[str, int] = dict(self._config_resolved.resources) if self._config_resolved.resources else {} + # Epoch state (persists after epoch finishes in netsim) self._epochs: dict[str, _EpochState] = {} # epoch_id -> _EpochState @@ -1860,6 +1868,8 @@ def _filter_startable_epochs( allowed = [] # Track how many new epochs we're allowing per node (within this batch) new_per_node: dict[str, int] = {} + # Track resource slots claimed by newly-allowed epochs (within this batch) + new_resource_usage: dict[str, int] = {} for epoch_id in epoch_ids: epoch = self._netsim.get_epoch(epoch_id) @@ -1886,7 +1896,36 @@ def _filter_startable_epochs( if current >= max_parallel: continue # Skip — at the limit + # Check depends_on: all dependency nodes must have completed at least one epoch + if config and config.depends_on: + if not all(dep in self._node_has_completed for dep in config.depends_on): + continue # Skip — dependency not yet satisfied + + # Check resources: all required resource slots must be available + if config and config.resources and self._resource_capacities: + can_acquire = True + for res_name, needed in config.resources.items(): + capacity = self._resource_capacities.get(res_name) + if capacity is None: + raise ValueError( + f"Node '{node_name}' requires resource '{res_name}' " + f"which is not defined in NetConfig.resources" + ) + current_usage = ( + self._resource_usage.get(res_name, 0) + + new_resource_usage.get(res_name, 0) + ) + if current_usage + needed > capacity: + can_acquire = False + break + if not can_acquire: + continue # Skip — resource limit reached + new_per_node[node_name] = new_per_node.get(node_name, 0) + 1 + # Track resource usage within this batch + if config and config.resources: + for res_name, needed in config.resources.items(): + new_resource_usage[res_name] = new_resource_usage.get(res_name, 0) + needed allowed.append(epoch_id) return allowed @@ -1965,6 +2004,8 @@ async def _finish_epoch_lifecycle(self, epoch_id: str, node_name: str, *, retry_ self._do_action(netrun_sim.NetAction.finish_epoch(epoch_id), epoch_id=epoch_id, detail={"node_name": node_name}) self._epochs[epoch_id].ended_at = get_timestamp_utc() self._epochs[epoch_id].state = netrun_sim.EpochState.Finished + self._epoch_states.pop(epoch_id, None) # Clear cross-retry state on success + self._node_has_completed.add(node_name) # Mark node as having completed (for depends_on) await self._fire_epoch_end(node_name, epoch_id, retry_count=retry_count) def _consume_epoch_inputs(self, epoch_id: str, node_name: str, packets: dict[str, list[str]], consumed_ports: set[str] | list[str]) -> None: @@ -2252,6 +2293,12 @@ async def _execute_epoch(self, epoch_id: str) -> NodeExecutionResult | None: # Transition epoch to Running self._running_epochs.add(epoch_id) + + # Acquire resources for this epoch + if config.resources: + for res_name, needed in config.resources.items(): + self._resource_usage[res_name] = self._resource_usage.get(res_name, 0) + needed + await self._start_epoch_lifecycle(epoch_id, node_name) try: @@ -2284,6 +2331,10 @@ async def _execute_epoch(self, epoch_id: str) -> NodeExecutionResult | None: raise finally: self._running_epochs.discard(epoch_id) + # Release resources + if config.resources: + for res_name, needed in config.resources.items(): + self._resource_usage[res_name] = self._resource_usage.get(res_name, 0) - needed async def _execute_epoch_with_retry( self, @@ -2324,6 +2375,10 @@ async def _execute_epoch_with_retry( # Get func key for this node func_key = self._get_func_key(node_name) + # Initialize cross-retry state on first attempt + if retry_count == 0: + self._epoch_states[epoch_id] = {} + # Dispatch to worker (with optional timeout) try: coro = self._execution_manager.run_allocate( @@ -2336,6 +2391,7 @@ async def _execute_epoch_with_retry( "retry_count": retry_count, "retry_timestamps": retry_timestamps, "retry_exceptions": retry_exceptions, + "state": self._epoch_states.get(epoch_id, {}), }, ) effective_timeout = resolve_effective_exec_field("timeout", config, self._config_resolved) @@ -2960,6 +3016,7 @@ async def _handle_epoch_failure( record.was_cancelled = True record.ended_at = get_timestamp_utc() record.destroyed_packets = list(response.destroyed_packets) + self._epoch_states.pop(epoch_id, None) # Clear cross-retry state on permanent failure await self._fire_epoch_end(node_name, epoch_id, error=error, retry_count=retry_count) # Store in dead letter queue diff --git a/netrun/src/netrun/net/config/_net_config.py b/netrun/src/netrun/net/config/_net_config.py index 50a28fe3..f0ad2a4a 100644 --- a/netrun/src/netrun/net/config/_net_config.py +++ b/netrun/src/netrun/net/config/_net_config.py @@ -283,6 +283,8 @@ def from_file( pools: dict[str, PoolConfig] | None = Field(default=None, description="Pool configurations. None generates a default main pool on resolve().") graph: GraphConfig + resources: dict[str, int] | None = Field(default=None, description="Named resource capacities as {resource_name: max_slots}. Nodes declare resource requirements in execution_config.resources. The scheduler ensures usage never exceeds capacity.") + extra: dict[str, Any] = Field(default_factory=dict, description="Arbitrary extra data (descriptions, version info, tool-specific data).") default_pool_allocation_method: RunAllocationMethod | VarRef = Field(default=RunAllocationMethod.ROUND_ROBIN, description="Default worker allocation method for nodes with multiple pools.") diff --git a/netrun/src/netrun/net/config/_nodes.py b/netrun/src/netrun/net/config/_nodes.py index f0f7177e..d79f4da9 100644 --- a/netrun/src/netrun/net/config/_nodes.py +++ b/netrun/src/netrun/net/config/_nodes.py @@ -232,6 +232,10 @@ class NodeExecutionConfig(VarResolvableModel): rate_limit_per_second: float | VarRef | None = Field(default=None, description="Maximum epoch triggers per second.") + depends_on: list[str] | None = Field(default=None, description="Node names that must have completed at least one epoch before this node can start. Provides directional ordering without explicit control edges.") + + resources: dict[str, int] | None = Field(default=None, description="Resource requirements as {resource_name: slots_needed}. Resources must be defined in NetConfig.resources with their capacity. Node epoch starts only when all required resource slots are available.") + defer_net_actions: bool | VarRef | None = Field(default=None, description="Defer net action notifications until epoch completes successfully. Required if retries enabled.") retries: int | VarRef | None = Field(default=None, description="Number of retry attempts on failure. None inherits from NetConfig.") diff --git a/netrun/src/tests/net/test_net.py b/netrun/src/tests/net/test_net.py index 850ad703..0d50e8d5 100644 --- a/netrun/src/tests/net/test_net.py +++ b/netrun/src/tests/net/test_net.py @@ -1,6 +1,6 @@ # AUTOGENERATED! DO NOT EDIT! File to edit: pts/tests/06_net/test_net.pct.py -__all__ = ['create_simple_graph_config', 'create_simple_net_config', 'test_async_epoch_callback', 'test_async_error_on_thread_pool', 'test_async_exec_func_on_main_pool', 'test_async_exec_func_on_thread_pool', 'test_async_from_function_factory_on_thread_pool', 'test_async_retry_on_thread_pool', 'test_async_subprocess_on_thread_pool', 'test_check_type_distinguishes_modes', 'test_check_type_string_generic_fails', 'test_check_type_string_simple', 'test_check_type_type_object_generic', 'test_check_type_type_object_generic_mismatch', 'test_check_type_type_object_simple', 'test_close_node_func_called_on_net_stop', 'test_concurrent_async_siblings_on_main_pool', 'test_context_cancel_epoch', 'test_context_consume_packet', 'test_context_consume_packet_not_found', 'test_context_create_packet', 'test_context_create_packet_from_value_func', 'test_context_full_workflow', 'test_context_get_execution_result', 'test_context_load_output_port', 'test_context_print_accumulates', 'test_context_print_basic', 'test_context_print_custom_separators', 'test_context_print_echo_stdout', 'test_context_print_empty', 'test_context_print_flush_ignored', 'test_context_print_multiple_timestamps', 'test_context_print_non_string', 'test_context_send_output_salvo', 'test_create_func_preprocessor_from_config', 'test_create_func_preprocessor_from_config_empty', 'test_create_net_func_preprocessor_basic', 'test_create_net_func_preprocessor_captures_exception', 'test_create_net_func_preprocessor_captures_prints', 'test_create_net_func_preprocessor_with_retry_info', 'test_ctx_vars_access', 'test_ctx_vars_empty', 'test_ctx_vars_inherit_missing_global_raises', 'test_ctx_vars_merging', 'test_ctx_vars_merging_with_inherit', 'test_dead_letter_queue_after_max_retries', 'test_dead_letter_queue_has_pool_and_worker', 'test_defer_init_delays_init_node_func', 'test_deferred_actions_preserved_in_result', 'test_deferred_queue_add_consume_packet', 'test_deferred_queue_add_create_packet', 'test_deferred_queue_add_load_output_port', 'test_deferred_queue_add_send_output_salvo', 'test_deferred_queue_discard', 'test_deferred_queue_multiple_creates', 'test_enable_disable_unknown_node', 'test_epoch_callback_deregistration', 'test_epoch_cancelled_can_be_raised_and_caught', 'test_epoch_cancelled_exception', 'test_epoch_end_on_cancelled', 'test_epoch_error_basic', 'test_epoch_error_can_be_caught', 'test_epoch_error_chaining', 'test_epoch_error_minimal', 'test_epoch_error_str', 'test_epoch_execution_simple_node', 'test_epoch_execution_with_output', 'test_epoch_record_cancellation_lifecycle', 'test_epoch_record_lifecycle_timestamps', 'test_epoch_record_out_salvos_populated', 'test_exception_queue_contains_epoch_error', 'test_execute_node_calls_init_node_func', 'test_execute_node_inside_net_basic', 'test_execute_node_inside_net_source_node', 'test_execute_node_outside_net', 'test_execute_node_outside_net_async_func', 'test_execute_node_salvo_condition_not_satisfied', 'test_execution_manager_get_pool', 'test_factory_node_exec_func_override', 'test_faulthandler_enabled_after_init', 'test_global_max_epochs_default_unlimited', 'test_global_max_epochs_limits_all_nodes', 'test_init_node_func_called_on_net_start', 'test_init_node_func_called_once_with_defer_init', 'test_is_blocked_with_disabled_node', 'test_max_epochs_allows_up_to_limit', 'test_max_epochs_exceeded_exception', 'test_max_epochs_none_unlimited', 'test_max_epochs_one_raises_on_second', 'test_max_epochs_queued_when_not_propagating', 'test_max_parallel_epochs_limits_concurrent_starts', 'test_max_parallel_epochs_none_allows_all', 'test_max_parallel_epochs_per_node', 'test_multiple_epoch_callbacks', 'test_net_check_rate_limit_enforced', 'test_net_check_rate_limit_no_config', 'test_net_check_rate_limit_none_limit', 'test_net_check_rate_limit_window_expires', 'test_net_clear_dead_letter_queue', 'test_net_close_sync', 'test_net_config_default_pool_allocation_method', 'test_net_config_default_pool_allocation_method_default', 'test_net_config_property', 'test_net_config_type_checking', 'test_net_context_manager', 'test_net_creation', 'test_net_dead_letter_queue_empty', 'test_net_dead_letter_queue_returns_copy', 'test_net_from_file_json', 'test_net_from_file_not_found', 'test_net_from_file_unsupported_format', 'test_net_get_epoch_log_empty', 'test_net_get_node_logs_empty', 'test_net_get_startable_epochs', 'test_net_graph_property', 'test_net_handle_print_buffer', 'test_net_init_sync', 'test_net_install_sigint_handler', 'test_net_invalid_pool_type_raises', 'test_net_is_blocked_empty_network', 'test_net_is_blocked_with_running_epochs', 'test_net_pause_and_resume', 'test_net_protocol_keys_uniqueness', 'test_net_protocol_keys_values', 'test_net_run_step', 'test_net_run_until_blocked', 'test_net_running_epochs_empty', 'test_net_start_and_stop', 'test_net_start_background', 'test_net_start_background_already_running', 'test_net_start_twice_raises', 'test_net_with_multiple_pool_types', 'test_net_with_node_execution_configs', 'test_node_disable_at_runtime', 'test_node_disabled_via_config', 'test_node_enable_at_runtime', 'test_node_execution_config_defaults', 'test_node_execution_config_max_epochs_default', 'test_node_execution_config_max_epochs_set', 'test_node_execution_config_new_fields', 'test_node_execution_config_retry_defaults', 'test_node_execution_config_type_checking', 'test_node_execution_config_with_retries', 'test_node_execution_context_creation', 'test_node_execution_context_with_retry_info', 'test_node_execution_result_with_exception', 'test_node_execution_result_with_func_result', 'test_node_failure_context_creation', 'test_node_failure_context_full', 'test_node_failure_context_has_pool_and_worker', 'test_node_failure_context_print', 'test_node_level_override_print', 'test_node_level_override_propagate', 'test_node_scoped_epoch_callbacks', 'test_on_epoch_end_callback', 'test_on_epoch_start_callback', 'test_on_node_failure_callback', 'test_per_node_max_epochs_overrides_global', 'test_per_node_minus_one_overrides_global_limit', 'test_pool_server_context_as_context_manager_still_works', 'test_pool_server_context_log_no_file', 'test_pool_server_context_start_stop', 'test_pool_server_context_start_stop_with_log_file', 'test_preprocessor_handles_cancel_epoch', 'test_preprocessor_type_checking_inheritance', 'test_print_exceptions_includes_pool_worker', 'test_print_exceptions_true_prints_to_stderr', 'test_propagate_exceptions_false_queues', 'test_propagate_exceptions_true_raises', 'test_propagate_exceptions_wraps_in_epoch_error', 'test_request_pool_shutdown_end_to_end', 'test_request_pool_shutdown_non_remote_raises', 'test_retry_calls_gc_collect', 'test_retry_on_failure', 'test_run_on_init_basic', 'test_run_on_init_disabled', 'test_run_on_init_error_no_valid_condition', 'test_run_on_init_outputs_flow_downstream', 'test_run_on_init_skip_via_constructor', 'test_run_on_init_skip_via_start_param', 'test_run_on_init_start_param_overrides_constructor', 'test_serve_pool_accepts_path', 'test_serve_pool_accepts_pathlib_path', 'test_serve_pool_custom_worker_name', 'test_serve_pool_end_to_end_with_net', 'test_serve_pool_log_file_writes', 'test_serve_pool_no_log_file', 'test_serve_pool_returns_pool_server_context', 'test_serve_pool_starts_and_stops_server', 'test_serve_pool_with_log_file', 'test_server_log_callback', 'test_start_stop_with_async_funcs', 'test_stop_not_called_for_unstarted_deferred_node', 'test_streaming_downstream_starts_before_slow_sibling', 'test_streaming_error_propagates', 'test_streaming_linear_pipeline', 'test_streaming_max_parallel_epochs_across_rounds', 'test_timeout_enforcement_raises_epoch_error', 'test_timeout_goes_to_dead_letter_queue', 'test_timeout_none_no_limit', 'test_type_checking_disabled'] +__all__ = ['create_simple_graph_config', 'create_simple_net_config', 'test_async_epoch_callback', 'test_async_error_on_thread_pool', 'test_async_exec_func_on_main_pool', 'test_async_exec_func_on_thread_pool', 'test_async_from_function_factory_on_thread_pool', 'test_async_retry_on_thread_pool', 'test_async_subprocess_on_thread_pool', 'test_check_type_distinguishes_modes', 'test_check_type_string_generic_fails', 'test_check_type_string_simple', 'test_check_type_type_object_generic', 'test_check_type_type_object_generic_mismatch', 'test_check_type_type_object_simple', 'test_close_node_func_called_on_net_stop', 'test_concurrent_async_siblings_on_main_pool', 'test_context_cancel_epoch', 'test_context_consume_packet', 'test_context_consume_packet_not_found', 'test_context_create_packet', 'test_context_create_packet_from_value_func', 'test_context_full_workflow', 'test_context_get_execution_result', 'test_context_load_output_port', 'test_context_print_accumulates', 'test_context_print_basic', 'test_context_print_custom_separators', 'test_context_print_echo_stdout', 'test_context_print_empty', 'test_context_print_flush_ignored', 'test_context_print_multiple_timestamps', 'test_context_print_non_string', 'test_context_send_output_salvo', 'test_create_func_preprocessor_from_config', 'test_create_func_preprocessor_from_config_empty', 'test_create_net_func_preprocessor_basic', 'test_create_net_func_preprocessor_captures_exception', 'test_create_net_func_preprocessor_captures_prints', 'test_create_net_func_preprocessor_with_retry_info', 'test_ctx_state_empty_by_default', 'test_ctx_state_persists_across_retries', 'test_ctx_vars_access', 'test_ctx_vars_empty', 'test_ctx_vars_inherit_missing_global_raises', 'test_ctx_vars_merging', 'test_ctx_vars_merging_with_inherit', 'test_dead_letter_queue_after_max_retries', 'test_dead_letter_queue_has_pool_and_worker', 'test_defer_init_delays_init_node_func', 'test_deferred_actions_preserved_in_result', 'test_deferred_queue_add_consume_packet', 'test_deferred_queue_add_create_packet', 'test_deferred_queue_add_load_output_port', 'test_deferred_queue_add_send_output_salvo', 'test_deferred_queue_discard', 'test_deferred_queue_multiple_creates', 'test_depends_on_blocks_until_dependency_completes', 'test_enable_disable_unknown_node', 'test_epoch_callback_deregistration', 'test_epoch_cancelled_can_be_raised_and_caught', 'test_epoch_cancelled_exception', 'test_epoch_end_on_cancelled', 'test_epoch_error_basic', 'test_epoch_error_can_be_caught', 'test_epoch_error_chaining', 'test_epoch_error_minimal', 'test_epoch_error_str', 'test_epoch_execution_simple_node', 'test_epoch_execution_with_output', 'test_epoch_record_cancellation_lifecycle', 'test_epoch_record_lifecycle_timestamps', 'test_epoch_record_out_salvos_populated', 'test_exception_queue_contains_epoch_error', 'test_execute_node_calls_init_node_func', 'test_execute_node_inside_net_basic', 'test_execute_node_inside_net_source_node', 'test_execute_node_outside_net', 'test_execute_node_outside_net_async_func', 'test_execute_node_salvo_condition_not_satisfied', 'test_execution_manager_get_pool', 'test_factory_node_exec_func_override', 'test_faulthandler_enabled_after_init', 'test_global_max_epochs_default_unlimited', 'test_global_max_epochs_limits_all_nodes', 'test_init_node_func_called_on_net_start', 'test_init_node_func_called_once_with_defer_init', 'test_is_blocked_with_disabled_node', 'test_max_epochs_allows_up_to_limit', 'test_max_epochs_exceeded_exception', 'test_max_epochs_none_unlimited', 'test_max_epochs_one_raises_on_second', 'test_max_epochs_queued_when_not_propagating', 'test_max_parallel_epochs_limits_concurrent_starts', 'test_max_parallel_epochs_none_allows_all', 'test_max_parallel_epochs_per_node', 'test_multiple_epoch_callbacks', 'test_net_check_rate_limit_enforced', 'test_net_check_rate_limit_no_config', 'test_net_check_rate_limit_none_limit', 'test_net_check_rate_limit_window_expires', 'test_net_clear_dead_letter_queue', 'test_net_close_sync', 'test_net_config_default_pool_allocation_method', 'test_net_config_default_pool_allocation_method_default', 'test_net_config_property', 'test_net_config_type_checking', 'test_net_context_manager', 'test_net_creation', 'test_net_dead_letter_queue_empty', 'test_net_dead_letter_queue_returns_copy', 'test_net_from_file_json', 'test_net_from_file_not_found', 'test_net_from_file_unsupported_format', 'test_net_get_epoch_log_empty', 'test_net_get_node_logs_empty', 'test_net_get_startable_epochs', 'test_net_graph_property', 'test_net_handle_print_buffer', 'test_net_init_sync', 'test_net_install_sigint_handler', 'test_net_invalid_pool_type_raises', 'test_net_is_blocked_empty_network', 'test_net_is_blocked_with_running_epochs', 'test_net_pause_and_resume', 'test_net_protocol_keys_uniqueness', 'test_net_protocol_keys_values', 'test_net_run_step', 'test_net_run_until_blocked', 'test_net_running_epochs_empty', 'test_net_start_and_stop', 'test_net_start_background', 'test_net_start_background_already_running', 'test_net_start_twice_raises', 'test_net_with_multiple_pool_types', 'test_net_with_node_execution_configs', 'test_node_disable_at_runtime', 'test_node_disabled_via_config', 'test_node_enable_at_runtime', 'test_node_execution_config_defaults', 'test_node_execution_config_max_epochs_default', 'test_node_execution_config_max_epochs_set', 'test_node_execution_config_new_fields', 'test_node_execution_config_retry_defaults', 'test_node_execution_config_type_checking', 'test_node_execution_config_with_retries', 'test_node_execution_context_creation', 'test_node_execution_context_with_retry_info', 'test_node_execution_result_with_exception', 'test_node_execution_result_with_func_result', 'test_node_failure_context_creation', 'test_node_failure_context_full', 'test_node_failure_context_has_pool_and_worker', 'test_node_failure_context_print', 'test_node_level_override_print', 'test_node_level_override_propagate', 'test_node_scoped_epoch_callbacks', 'test_on_epoch_end_callback', 'test_on_epoch_start_callback', 'test_on_node_failure_callback', 'test_per_node_max_epochs_overrides_global', 'test_per_node_minus_one_overrides_global_limit', 'test_pool_server_context_as_context_manager_still_works', 'test_pool_server_context_log_no_file', 'test_pool_server_context_start_stop', 'test_pool_server_context_start_stop_with_log_file', 'test_preprocessor_handles_cancel_epoch', 'test_preprocessor_type_checking_inheritance', 'test_print_exceptions_includes_pool_worker', 'test_print_exceptions_true_prints_to_stderr', 'test_propagate_exceptions_false_queues', 'test_propagate_exceptions_true_raises', 'test_propagate_exceptions_wraps_in_epoch_error', 'test_request_pool_shutdown_end_to_end', 'test_request_pool_shutdown_non_remote_raises', 'test_resources_mutual_exclusion', 'test_resources_none_is_noop', 'test_retry_calls_gc_collect', 'test_retry_on_failure', 'test_run_on_init_basic', 'test_run_on_init_disabled', 'test_run_on_init_error_no_valid_condition', 'test_run_on_init_outputs_flow_downstream', 'test_run_on_init_skip_via_constructor', 'test_run_on_init_skip_via_start_param', 'test_run_on_init_start_param_overrides_constructor', 'test_serve_pool_accepts_path', 'test_serve_pool_accepts_pathlib_path', 'test_serve_pool_custom_worker_name', 'test_serve_pool_end_to_end_with_net', 'test_serve_pool_log_file_writes', 'test_serve_pool_no_log_file', 'test_serve_pool_returns_pool_server_context', 'test_serve_pool_starts_and_stops_server', 'test_serve_pool_with_log_file', 'test_server_log_callback', 'test_start_stop_with_async_funcs', 'test_stop_not_called_for_unstarted_deferred_node', 'test_streaming_downstream_starts_before_slow_sibling', 'test_streaming_error_propagates', 'test_streaming_linear_pipeline', 'test_streaming_max_parallel_epochs_across_rounds', 'test_timeout_enforcement_raises_epoch_error', 'test_timeout_goes_to_dead_letter_queue', 'test_timeout_none_no_limit', 'test_type_checking_disabled'] # %% pts/tests/06_net/test_net.pct.py 2 import pytest @@ -6472,3 +6472,327 @@ def failing_then_succeeds(ctx, packets): # The node should have succeeded on 3rd attempt assert call_count[0] == 3 + +# %% pts/tests/06_net/test_net.pct.py 360 +@pytest.mark.asyncio +async def test_ctx_state_persists_across_retries(): + """Test that ctx.state is preserved across retry attempts.""" + call_log = [] + + def stateful_node(ctx, packets): + for port_name, pkt_ids in packets.items(): + for pid in pkt_ids: + ctx.consume_packet(pid) + + if "initialized" not in ctx.state: + ctx.state["initialized"] = True + ctx.state["counter"] = 0 + + ctx.state["counter"] += 1 + call_log.append({"retry": ctx.retry_count, "counter": ctx.state["counter"]}) + + if ctx.retry_count < 2: + raise ValueError(f"Fail on retry {ctx.retry_count}") + + graph_config = GraphConfig( + nodes=[ + NodeConfig( + name="Stateful", + in_ports={"in": PortConfig()}, + in_salvo_conditions={ + "default": SalvoConditionConfig( + max_salvos=MaxSalvosFiniteConfig(max=1), + ports={"in": PacketCountAllConfig()}, + term=SalvoConditionTermPortConfig( + port_name="in", + state=PortStateNonEmptyConfig(), + ), + ), + }, + execution_config=NodeExecutionConfig( + node_name="Stateful", + pools=["main"], + exec_node_func=stateful_node, + retries=3, + retry_wait=0.0, + ), + ), + ], + edges=[], + ) + config = NetConfig( + pools={"main": PoolConfig(spec=MainPoolConfig())}, + graph=graph_config, + ) + + async with Net(config) as net: + net.inject_data("Stateful", "in", [1]) + await net.run_until_blocked() + + # State should have persisted — counter incremented on each attempt + assert len(call_log) == 3 + assert call_log[0]["counter"] == 1 # First attempt + assert call_log[1]["counter"] == 2 # Second attempt (same dict) + assert call_log[2]["counter"] == 3 # Third attempt (same dict) + +# %% pts/tests/06_net/test_net.pct.py 362 +@pytest.mark.asyncio +async def test_ctx_state_empty_by_default(): + """Test that ctx.state is an empty dict when not used.""" + observed_state = [None] + + def check_state(ctx, packets): + for port_name, pkt_ids in packets.items(): + for pid in pkt_ids: + ctx.consume_packet(pid) + observed_state[0] = dict(ctx.state) + + graph_config = GraphConfig( + nodes=[ + NodeConfig( + name="CheckState", + in_ports={"in": PortConfig()}, + in_salvo_conditions={ + "default": SalvoConditionConfig( + max_salvos=MaxSalvosFiniteConfig(max=1), + ports={"in": PacketCountAllConfig()}, + term=SalvoConditionTermPortConfig( + port_name="in", + state=PortStateNonEmptyConfig(), + ), + ), + }, + execution_config=NodeExecutionConfig( + node_name="CheckState", + pools=["main"], + exec_node_func=check_state, + ), + ), + ], + edges=[], + ) + config = NetConfig( + pools={"main": PoolConfig(spec=MainPoolConfig())}, + graph=graph_config, + ) + + async with Net(config) as net: + net.inject_data("CheckState", "in", [1]) + await net.run_until_blocked() + + assert observed_state[0] == {} + +# %% pts/tests/06_net/test_net.pct.py 365 +@pytest.mark.asyncio +async def test_depends_on_blocks_until_dependency_completes(): + """Test that depends_on prevents a node from starting until its dependency completes.""" + execution_order = [] + + def node_a(ctx, packets): + for port_name, pkt_ids in packets.items(): + for pid in pkt_ids: + ctx.consume_packet(pid) + execution_order.append("A") + + def node_b(ctx, packets): + for port_name, pkt_ids in packets.items(): + for pid in pkt_ids: + ctx.consume_packet(pid) + execution_order.append("B") + + graph_config = GraphConfig( + nodes=[ + NodeConfig( + name="A", + in_ports={"in": PortConfig()}, + in_salvo_conditions={ + "default": SalvoConditionConfig( + max_salvos=MaxSalvosFiniteConfig(max=1), + ports={"in": PacketCountAllConfig()}, + term=SalvoConditionTermPortConfig( + port_name="in", + state=PortStateNonEmptyConfig(), + ), + ), + }, + execution_config=NodeExecutionConfig( + node_name="A", + pools=["main"], + exec_node_func=node_a, + ), + ), + NodeConfig( + name="B", + in_ports={"in": PortConfig()}, + in_salvo_conditions={ + "default": SalvoConditionConfig( + max_salvos=MaxSalvosFiniteConfig(max=1), + ports={"in": PacketCountAllConfig()}, + term=SalvoConditionTermPortConfig( + port_name="in", + state=PortStateNonEmptyConfig(), + ), + ), + }, + execution_config=NodeExecutionConfig( + node_name="B", + pools=["main"], + exec_node_func=node_b, + depends_on=["A"], + ), + ), + ], + edges=[], + ) + config = NetConfig( + pools={"main": PoolConfig(spec=MainPoolConfig())}, + graph=graph_config, + ) + + async with Net(config) as net: + # Inject data for both nodes + net.inject_data("A", "in", [1]) + net.inject_data("B", "in", [2]) + await net.run_until_blocked() + + # A must execute before B (B depends_on A) + assert execution_order == ["A", "B"] + +# %% pts/tests/06_net/test_net.pct.py 368 +@pytest.mark.asyncio +async def test_resources_mutual_exclusion(): + """Test that two nodes with the same 1-slot resource never run concurrently.""" + import time as _time + + timestamps = {} + + async def heavy_a(ctx, packets): + for port_name, pkt_ids in packets.items(): + for pid in pkt_ids: + ctx.consume_packet(pid) + timestamps["a_start"] = _time.monotonic() + await asyncio.sleep(0.1) + timestamps["a_end"] = _time.monotonic() + + async def heavy_b(ctx, packets): + for port_name, pkt_ids in packets.items(): + for pid in pkt_ids: + ctx.consume_packet(pid) + timestamps["b_start"] = _time.monotonic() + await asyncio.sleep(0.1) + timestamps["b_end"] = _time.monotonic() + + graph_config = GraphConfig( + nodes=[ + NodeConfig( + name="HeavyA", + in_ports={"in": PortConfig()}, + in_salvo_conditions={ + "default": SalvoConditionConfig( + max_salvos=MaxSalvosFiniteConfig(max=1), + ports={"in": PacketCountAllConfig()}, + term=SalvoConditionTermPortConfig( + port_name="in", + state=PortStateNonEmptyConfig(), + ), + ), + }, + execution_config=NodeExecutionConfig( + node_name="HeavyA", + pools=["main"], + exec_node_func=heavy_a, + resources={"heavy_mem": 1}, + ), + ), + NodeConfig( + name="HeavyB", + in_ports={"in": PortConfig()}, + in_salvo_conditions={ + "default": SalvoConditionConfig( + max_salvos=MaxSalvosFiniteConfig(max=1), + ports={"in": PacketCountAllConfig()}, + term=SalvoConditionTermPortConfig( + port_name="in", + state=PortStateNonEmptyConfig(), + ), + ), + }, + execution_config=NodeExecutionConfig( + node_name="HeavyB", + pools=["main"], + exec_node_func=heavy_b, + resources={"heavy_mem": 1}, + ), + ), + ], + edges=[], + ) + config = NetConfig( + pools={"main": PoolConfig(spec=MainPoolConfig())}, + resources={"heavy_mem": 1}, + graph=graph_config, + ) + + async with Net(config) as net: + net.inject_data("HeavyA", "in", [1]) + net.inject_data("HeavyB", "in", [2]) + await net.run_until_blocked() + + # Both should have completed + assert "a_end" in timestamps and "b_end" in timestamps + + # They must NOT have overlapped (mutual exclusion via 1-slot resource) + # One must have finished before the other started + a_before_b = timestamps["a_end"] <= timestamps["b_start"] + b_before_a = timestamps["b_end"] <= timestamps["a_start"] + assert a_before_b or b_before_a, ( + f"HeavyA and HeavyB overlapped: A=[{timestamps['a_start']:.3f}, {timestamps['a_end']:.3f}], " + f"B=[{timestamps['b_start']:.3f}, {timestamps['b_end']:.3f}]" + ) + +# %% pts/tests/06_net/test_net.pct.py 370 +@pytest.mark.asyncio +async def test_resources_none_is_noop(): + """Test that nodes without resources work as before.""" + executed = [False] + + def simple_node(ctx, packets): + for port_name, pkt_ids in packets.items(): + for pid in pkt_ids: + ctx.consume_packet(pid) + executed[0] = True + + graph_config = GraphConfig( + nodes=[ + NodeConfig( + name="Simple", + in_ports={"in": PortConfig()}, + in_salvo_conditions={ + "default": SalvoConditionConfig( + max_salvos=MaxSalvosFiniteConfig(max=1), + ports={"in": PacketCountAllConfig()}, + term=SalvoConditionTermPortConfig( + port_name="in", + state=PortStateNonEmptyConfig(), + ), + ), + }, + execution_config=NodeExecutionConfig( + node_name="Simple", + pools=["main"], + exec_node_func=simple_node, + ), + ), + ], + edges=[], + ) + config = NetConfig( + pools={"main": PoolConfig(spec=MainPoolConfig())}, + graph=graph_config, + ) + + async with Net(config) as net: + net.inject_data("Simple", "in", [1]) + await net.run_until_blocked() + + assert executed[0] is True From 22b6404af92d03dcf86e9e84a2d0189d58aa03a2 Mon Sep 17 00:00:00 2001 From: Lukas Kikuchi Date: Sat, 25 Apr 2026 20:01:27 +0100 Subject: [PATCH 04/15] refactor(netrun): extract BasePool, simplify pool implementations Phase 3 of the major refactor. Introduces BasePool abstract base class that provides concrete recv(), try_recv(), start(), close(), and health monitoring. ThreadPool, MultiprocessPool, and SingleWorkerPool now extend BasePool, implementing only pool-specific logic. BasePool provides: - Unified recv/try_recv with error checking and timeout handling - Lazy recv task startup with _create_recv_loops() override - Health monitoring via _check_worker_health() override - Worker silence detection (configurable threshold, logs warnings) - Common lifecycle management (start/close with proper ordering) - Context manager support Pool migrations: - SingleWorkerPool: 451 -> 310 lines (-31%) - ThreadPool: 458 -> 323 lines (-29%) - MultiprocessPool: 1102 -> 905 lines (-18%) Overrides close() for SHUTDOWN_COMPLETE protocol - RemotePoolClient: unchanged (different architecture) ExecutionManager cleanup: - Removed _thread_worker_func and _multiprocess_worker_func wrappers (inline functools.partial at call sites) - Replaced O(n) round-robin list with O(1) counter Net reduction: -518 lines across pool+EM (853 added, 1371 removed) Co-Authored-By: Claude Opus 4.6 (1M context) --- netrun/pts/netrun/04_pool/00_base.pct.py | 231 ++++++++++ netrun/pts/netrun/04_pool/01_thread.pct.py | 233 ++-------- .../pts/netrun/04_pool/02_multiprocess.pct.py | 427 +++++------------- netrun/pts/netrun/04_pool/04_aio.pct.py | 199 ++------ netrun/pts/netrun/05_execution_manager.pct.py | 25 +- netrun/src/netrun/execution_manager.py | 25 +- netrun/src/netrun/pool/aio.py | 199 ++------ netrun/src/netrun/pool/base.py | 225 ++++++++- netrun/src/netrun/pool/multiprocess.py | 427 +++++------------- netrun/src/netrun/pool/thread.py | 233 ++-------- 10 files changed, 853 insertions(+), 1371 deletions(-) diff --git a/netrun/pts/netrun/04_pool/00_base.pct.py b/netrun/pts/netrun/04_pool/00_base.pct.py index 7117ccd1..91fc2b77 100644 --- a/netrun/pts/netrun/04_pool/00_base.pct.py +++ b/netrun/pts/netrun/04_pool/00_base.pct.py @@ -258,3 +258,234 @@ def _check_error_and_raise(msg: WorkerMessage) -> None: raise WorkerCrashed(msg.worker_id, msg.data) elif msg.key == POOL_UP_ERROR_TIMEOUT: raise WorkerTimeout(msg.worker_id, msg.data["timeout_seconds"]) + +# %% [markdown] +# ## BasePool +# +# Abstract base class that provides the common lifecycle, recv/try_recv, +# monitor, and silence detection. Subclasses implement pool-specific +# worker creation, shutdown, message routing, and health checking. + +# %% +#|export +import asyncio +import time +import logging +from abc import ABC, abstractmethod +from collections.abc import Coroutine + +class BasePool(ABC): + """Abstract base class for worker pools. + + Provides concrete implementations of recv(), try_recv(), start(), close(), + and worker health monitoring with optional silence detection. Subclasses + implement the pool-specific worker lifecycle and message routing. + + Subclass contract: + _do_start() — Create workers and channels + _do_close(timeout) — Shut down workers, clean up resources + _create_recv_loops() — Return coroutines that forward messages to _recv_queue + _check_worker_health() — Detect dead workers, put errors on _recv_queue + send(worker_id, ...) — Send to a specific worker + broadcast(key, data) — Send to all workers + """ + + def __init__(self, num_workers: int, *, silence_timeout: float | None = None): + """Initialize the base pool. + + Args: + num_workers: Number of workers in this pool. + silence_timeout: If set, log a warning when any worker has been + silent for this many seconds. None disables silence detection. + """ + self._num_workers = num_workers + self._running = False + self._recv_queue: asyncio.Queue[WorkerMessage] = asyncio.Queue() + self._recv_tasks: list[asyncio.Task] = [] + self._monitor_task: asyncio.Task | None = None + self._silence_timeout = silence_timeout + self._last_message_time: dict[int, float] = {} + + @property + def num_workers(self) -> int: + return self._num_workers + + @property + def is_running(self) -> bool: + return self._running + + async def start(self) -> None: + """Start the pool: create workers, begin health monitoring.""" + if self._running: + raise PoolAlreadyStarted("Pool is already running") + await self._do_start() + self._running = True + self._monitor_task = asyncio.create_task(self._monitor_loop()) + + async def close(self, timeout: float | None = None) -> None: + """Shut down the pool: stop workers, cancel tasks, clean up.""" + if not self._running: + return + # Ensure recv tasks are running before setting _running=False. + # MultiprocessPool needs recv tasks to receive SHUTDOWN_COMPLETE. + self._start_recv_tasks() + self._running = False + # Cancel monitor + if self._monitor_task is not None and not self._monitor_task.done(): + self._monitor_task.cancel() + try: + await self._monitor_task + except asyncio.CancelledError: + pass + self._monitor_task = None + # Subclass shutdown (send shutdown signals, wait, close channels, join) + await self._do_close(timeout) + # Cancel recv tasks + for task in self._recv_tasks: + if not task.done(): + task.cancel() + try: + await task + except asyncio.CancelledError: + pass + self._recv_tasks.clear() + self._recv_queue = asyncio.Queue() + + def _start_recv_tasks(self) -> None: + """Start background tasks that forward worker messages to _recv_queue. + + Idempotent — only creates tasks on first call. + """ + if self._recv_tasks: + return + for coro in self._create_recv_loops(): + self._recv_tasks.append(asyncio.create_task(coro)) + + async def recv(self, timeout: float | None = None) -> WorkerMessage: + """Receive a message from any worker. + + Raises: + PoolNotStarted: If pool hasn't been started + RecvTimeout: If timeout expires + WorkerException: If a worker raised an exception + WorkerCrashed: If a worker died unexpectedly + WorkerTimeout: If a worker timed out + """ + if not self._running: + raise PoolNotStarted("Pool has not been started") + self._start_recv_tasks() + try: + if timeout is None: + msg = await self._recv_queue.get() + else: + msg = await asyncio.wait_for( + self._recv_queue.get(), + timeout=timeout, + ) + except (TimeoutError, asyncio.TimeoutError): + from netrun.rpc.base import RecvTimeout + raise RecvTimeout(f"Receive timed out after {timeout}s") + self._last_message_time[msg.worker_id] = time.monotonic() + _check_error_and_raise(msg) + return msg + + async def try_recv(self) -> WorkerMessage | None: + """Non-blocking receive from any worker. + + If recv tasks are running (started by a previous recv() call), + checks the centralized queue. Otherwise, delegates to + _try_recv_direct() which subclasses can override to read + directly from channels. + + Raises: + PoolNotStarted: If pool hasn't been started + WorkerException: If a worker raised an exception + WorkerCrashed: If a worker died unexpectedly + WorkerTimeout: If a worker timed out + """ + if not self._running: + raise PoolNotStarted("Pool has not been started") + if self._recv_tasks: + try: + msg = self._recv_queue.get_nowait() + except asyncio.QueueEmpty: + return None + else: + msg = await self._try_recv_direct() + if msg is None: + return None + self._last_message_time[msg.worker_id] = time.monotonic() + _check_error_and_raise(msg) + return msg + + async def _monitor_loop(self) -> None: + """Background task: check worker health and silence detection.""" + _logger = logging.getLogger("netrun.pool") + while self._running: + await self._check_worker_health() + # Silence detection + if self._silence_timeout is not None: + now = time.monotonic() + for worker_id in range(self._num_workers): + last = self._last_message_time.get(worker_id) + if last is not None and (now - last) > self._silence_timeout: + _logger.warning( + "Worker %d has been silent for %.0fs (threshold: %.0fs)", + worker_id, now - last, self._silence_timeout, + ) + self._last_message_time[worker_id] = now # Reset to avoid spam + await asyncio.sleep(0.5) + + # --- Subclass contract --- + + @abstractmethod + async def _do_start(self) -> None: + """Create workers and channels. Called by start().""" + ... + + @abstractmethod + async def _do_close(self, timeout: float | None = None) -> None: + """Shut down workers and clean up. Called by close() after monitor is cancelled.""" + ... + + @abstractmethod + def _create_recv_loops(self) -> list[Coroutine]: + """Return coroutines that read from workers and put WorkerMessages on _recv_queue. + + Each coroutine should loop reading from its worker channel and forwarding + to self._recv_queue. It should catch ChannelClosed and CancelledError + and exit gracefully. + """ + ... + + @abstractmethod + async def _check_worker_health(self) -> None: + """Check if any workers have died. Put error WorkerMessages on _recv_queue for dead workers.""" + ... + + async def _try_recv_direct(self) -> WorkerMessage | None: + """Direct non-blocking receive (when recv tasks aren't running). + + Default implementation returns None. Subclasses that support direct + channel reads (ThreadPool, SingleWorkerPool) can override. + """ + return None + + @abstractmethod + async def send(self, worker_id: WorkerId, key: str, data: Any) -> None: + """Send a message to a specific worker.""" + ... + + @abstractmethod + async def broadcast(self, key: str, data: Any) -> None: + """Send a message to all workers.""" + ... + + # --- Context manager --- + + async def __aenter__(self): + await self.start() + return self + + async def __aexit__(self, *args): + await self.close() diff --git a/netrun/pts/netrun/04_pool/01_thread.pct.py b/netrun/pts/netrun/04_pool/01_thread.pct.py index 049f87e5..f230b926 100644 --- a/netrun/pts/netrun/04_pool/01_thread.pct.py +++ b/netrun/pts/netrun/04_pool/01_thread.pct.py @@ -60,6 +60,7 @@ create_thread_channel_pair, ) from netrun.pool.base import ( + BasePool, WorkerId, WorkerFn, WorkerMessage, @@ -75,18 +76,14 @@ # %% #|export -class ThreadPool: +class ThreadPool(BasePool): """A pool of worker threads. Each worker runs a user-provided function that receives messages via a sync channel and can send responses back. """ - def __init__( - self, - worker_fn: WorkerFn, - num_workers: int, - ): + def __init__(self, worker_fn: WorkerFn, num_workers: int, **kwargs): """Create a thread pool. Args: @@ -95,43 +92,21 @@ def __init__( """ if num_workers < 1: raise ValueError("num_workers must be at least 1") - + super().__init__(num_workers=num_workers, **kwargs) self._worker_fn = worker_fn - self._num_workers = num_workers - self._running = False - - # Will be populated on start() self._channels: list[ThreadChannel] = [] self._threads: list[threading.Thread] = [] - self._recv_queue: asyncio.Queue = asyncio.Queue() - self._recv_tasks: list[asyncio.Task] = [] - self._monitor_task: asyncio.Task | None = None - self._dead_workers: set[int] = set() # Track workers we've already reported as dead - - @property - def num_workers(self) -> int: - """Total number of workers in the pool.""" - return self._num_workers - - @property - def is_running(self) -> bool: - """Whether the pool has been started.""" - return self._running - - async def start(self) -> None: - """Start all workers in the pool.""" - if self._running: - raise PoolAlreadyStarted("Pool is already running") + self._dead_workers: set[int] = set() + async def _do_start(self) -> None: self._channels = [] self._threads = [] + self._dead_workers = set() for worker_id in range(self._num_workers): - # Create channel pair parent_channel, child_queues = create_thread_channel_pair() self._channels.append(parent_channel) - # Create and start worker thread thread = threading.Thread( target=self._run_worker, args=(child_queues, worker_id), @@ -141,196 +116,86 @@ async def start(self) -> None: thread.start() self._threads.append(thread) - self._running = True - self._dead_workers = set() - self._monitor_task = asyncio.create_task(self._monitor_workers()) - - async def _monitor_workers(self) -> None: - """Background task to detect dead worker threads.""" - while self._running: - for worker_id, thread in enumerate(self._threads): - if worker_id not in self._dead_workers and not thread.is_alive(): - # Thread died - send crash notification - self._dead_workers.add(worker_id) - await self._recv_queue.put(WorkerMessage( - worker_id=worker_id, - key=POOL_UP_ERROR_CRASHED, - data={"reason": "Thread exited unexpectedly"} - )) - await asyncio.sleep(0.5) # Check every 500ms - def _run_worker(self, child_queues: tuple, worker_id: WorkerId) -> None: """Run the worker function in a thread.""" send_q, recv_q = child_queues channel = SyncThreadChannel(send_q, recv_q) - try: self._worker_fn(channel, worker_id) except ChannelClosed: pass except Exception as e: - # Try to send exception object back (no serialization needed for threads) try: channel.send(POOL_UP_ERROR_EXCEPTION, e) except Exception: pass - async def close(self, timeout: float | None = None) -> None: - """Shut down all workers and clean up resources. - - Args: - timeout: Max seconds to wait for each worker to finish gracefully. - If None, wait indefinitely. - """ - if not self._running: - return - - self._running = False - - # Cancel monitor task - if self._monitor_task and not self._monitor_task.done(): - self._monitor_task.cancel() - try: - await self._monitor_task - except asyncio.CancelledError: - pass - - # Close channels first - this unblocks any recv() calls + async def _do_close(self, timeout: float | None = None) -> None: + # Close channels first — unblocks any recv() calls in workers for channel in self._channels: await channel.close() - # Now cancel recv tasks (they should exit quickly since channels are closed) - for task in self._recv_tasks: - if not task.done(): - task.cancel() - try: - await task - except asyncio.CancelledError: - pass - # Wait for threads to finish for thread in self._threads: thread.join(timeout=timeout) self._channels = [] self._threads = [] - self._recv_queue = asyncio.Queue() - self._recv_tasks = [] - self._monitor_task = None self._dead_workers = set() - async def send(self, worker_id: WorkerId, key: str, data: Any) -> None: - """Send a message to a specific worker.""" - if not self._running: - raise PoolNotStarted("Pool has not been started") - - if worker_id < 0 or worker_id >= self._num_workers: - raise ValueError(f"worker_id {worker_id} out of range [0, {self._num_workers})") - - await self._channels[worker_id].send(key, data) - - def _start_recv_tasks(self) -> None: - """Start background tasks that forward messages to the queue.""" - if self._recv_tasks: - return + def _create_recv_loops(self) -> list: + loops = [] + for worker_id, channel in enumerate(self._channels): + async def recv_loop(_worker_id=worker_id, _channel=channel): + try: + while self._running: + key, data = await _channel.recv() + await self._recv_queue.put( + WorkerMessage(worker_id=_worker_id, key=key, data=data) + ) + except (ChannelClosed, asyncio.CancelledError): + pass + except Exception as e: + logging.getLogger("netrun.pool").error( + f"recv_loop for worker {_worker_id} crashed: {e}", exc_info=True + ) + await self._recv_queue.put(WorkerMessage( + worker_id=_worker_id, key=POOL_UP_ERROR_CRASHED, + data={"reason": f"recv_loop exception: {e}"}, + )) + loops.append(recv_loop()) + return loops - async def recv_loop(worker_id: WorkerId, channel: ThreadChannel): - try: - while self._running: - key, data = await channel.recv() - msg = WorkerMessage(worker_id=worker_id, key=key, data=data) - await self._recv_queue.put(msg) - except (ChannelClosed, asyncio.CancelledError): - pass - except Exception as e: - logging.getLogger("netrun.pool").error( - f"recv_loop for worker {worker_id} crashed: {e}", exc_info=True - ) + async def _check_worker_health(self) -> None: + for worker_id, thread in enumerate(self._threads): + if worker_id not in self._dead_workers and not thread.is_alive(): + self._dead_workers.add(worker_id) await self._recv_queue.put(WorkerMessage( - worker_id=worker_id, - key=POOL_UP_ERROR_CRASHED, - data={"reason": f"recv_loop exception: {e}"} + worker_id=worker_id, key=POOL_UP_ERROR_CRASHED, + data={"reason": "Thread exited unexpectedly"}, )) - for worker_id, channel in enumerate(self._channels): - task = asyncio.create_task(recv_loop(worker_id, channel)) - self._recv_tasks.append(task) - - async def recv(self, timeout: float | None = None) -> WorkerMessage: - """Receive a message from any worker. - - Raises: - WorkerException: If the worker raised an exception - WorkerCrashed: If the worker died unexpectedly - WorkerTimeout: If the worker timed out - RecvTimeout: If this recv() call times out - """ - if not self._running: - raise PoolNotStarted("Pool has not been started") - - self._start_recv_tasks() - - try: - if timeout is None: - msg = await self._recv_queue.get() - else: - msg = await asyncio.wait_for( - self._recv_queue.get(), - timeout=timeout, - ) - except TimeoutError: - raise RecvTimeout(f"Receive timed out after {timeout}s") - - _check_error_and_raise(msg) - return msg - - async def try_recv(self) -> WorkerMessage | None: - """Non-blocking receive from any worker. - - Raises: - WorkerException: If the worker raised an exception - WorkerCrashed: If the worker died unexpectedly - WorkerTimeout: If the worker timed out - """ - if not self._running: - raise PoolNotStarted("Pool has not been started") - - # If recv tasks are running, check the queue first - if self._recv_tasks: - try: - msg = self._recv_queue.get_nowait() - _check_error_and_raise(msg) - return msg - except asyncio.QueueEmpty: - return None - - # Otherwise, read directly from channels + async def _try_recv_direct(self) -> WorkerMessage | None: + """Read directly from channels (when recv tasks aren't running).""" for worker_id, channel in enumerate(self._channels): result = await channel.try_recv() if result is not None: key, data = result - msg = WorkerMessage(worker_id=worker_id, key=key, data=data) - _check_error_and_raise(msg) - return msg - + return WorkerMessage(worker_id=worker_id, key=key, data=data) return None - async def broadcast(self, key: str, data: Any) -> None: - """Send a message to all workers.""" + async def send(self, worker_id: WorkerId, key: str, data: Any) -> None: if not self._running: raise PoolNotStarted("Pool has not been started") + if worker_id < 0 or worker_id >= self._num_workers: + raise ValueError(f"worker_id {worker_id} out of range [0, {self._num_workers})") + await self._channels[worker_id].send(key, data) - for worker_id in range(self._num_workers): - await self._channels[worker_id].send(key, data) - - async def __aenter__(self) -> "ThreadPool": - """Context manager entry - starts the pool.""" - await self.start() - return self - - async def __aexit__(self, exc_type, exc_val, exc_tb) -> None: - """Context manager exit - closes the pool.""" - await self.close() + async def broadcast(self, key: str, data: Any) -> None: + if not self._running: + raise PoolNotStarted("Pool has not been started") + for channel in self._channels: + await channel.send(key, data) # %% [markdown] # ## Example: Echo Pool diff --git a/netrun/pts/netrun/04_pool/02_multiprocess.pct.py b/netrun/pts/netrun/04_pool/02_multiprocess.pct.py index 7c09458b..0ac5b78e 100644 --- a/netrun/pts/netrun/04_pool/02_multiprocess.pct.py +++ b/netrun/pts/netrun/04_pool/02_multiprocess.pct.py @@ -79,6 +79,7 @@ create_queue_pair, ) from netrun.pool.base import ( + BasePool, WorkerId, WorkerFn, WorkerMessage, @@ -516,7 +517,7 @@ def is_closed(self) -> bool: # %% #|export -class MultiprocessPool: +class MultiprocessPool(BasePool): """A pool of worker processes, each running multiple threads. Messages are routed to specific workers via their worker_id. @@ -532,182 +533,92 @@ def __init__( buffer_output: bool = True, output_flush_interval: float = 0.1, on_output: Callable[[int, OutputBuffer], None] | None = None, + **kwargs, ): - """Create a multiprocess pool. - - Args: - worker_fn: Function to run in each worker thread (must be importable) - num_processes: Number of subprocesses to create - threads_per_process: Number of worker threads per subprocess - redirect_output: If True, capture stdout/stderr from subprocesses - buffer_output: If True, buffer captured output for retrieval. - If False, discard captured output (silent mode). - Only applies when redirect_output=True. - output_flush_interval: Interval in seconds between automatic output buffer - flushes from subprocesses (default 0.1 = 100ms). - Only applies when redirect_output=True and buffer_output=True. - on_output: Optional callback called when output buffer is received from a - subprocess. Called with (process_idx, buffer) where buffer is - a list of (timestamp, is_stdout, text) tuples. - """ if num_processes < 1: raise ValueError("num_processes must be at least 1") if threads_per_process < 1: raise ValueError("threads_per_process must be at least 1") + super().__init__(num_workers=num_processes * threads_per_process, **kwargs) self._worker_fn = worker_fn self._num_processes = num_processes self._threads_per_process = threads_per_process - self._num_workers = num_processes * threads_per_process self._redirect_output = redirect_output self._buffer_output = buffer_output self._output_flush_interval = output_flush_interval self._on_output = on_output - self._running = False - # Will be populated on start() self._channels: list[ProcessChannel] = [] self._processes: list[mp.Process] = [] - self._recv_queue: asyncio.Queue = asyncio.Queue() - self._recv_tasks: list[asyncio.Task] = [] - self._monitor_task: asyncio.Task | None = None - self._dead_processes: set[int] = set() # Track processes we've already reported as dead - self._stdout_buffers: dict[int, OutputBuffer] = {} # process_idx -> buffer - self._flush_events: dict[int, asyncio.Event] = {} # process_idx -> event for flush response - self._shutdown_complete_events: dict[int, asyncio.Event] = {} # process_idx -> event for shutdown complete - - @property - def num_workers(self) -> int: - """Total number of workers in the pool.""" - return self._num_workers + self._dead_processes: set[int] = set() + self._stdout_buffers: dict[int, OutputBuffer] = {} + self._flush_events: dict[int, asyncio.Event] = {} + self._shutdown_complete_events: dict[int, asyncio.Event] = {} @property def num_processes(self) -> int: - """Number of subprocesses.""" return self._num_processes @property def threads_per_process(self) -> int: - """Number of threads per subprocess.""" return self._threads_per_process - @property - def is_running(self) -> bool: - """Whether the pool has been started.""" - return self._running - def _worker_id_to_process_thread(self, worker_id: WorkerId) -> tuple[int, int]: - """Convert flat worker_id to (process_idx, thread_idx).""" process_idx = worker_id // self._threads_per_process thread_idx = worker_id % self._threads_per_process return process_idx, thread_idx - async def start(self) -> None: - """Start all processes and workers.""" - if self._running: - raise PoolAlreadyStarted("Pool is already running") - + async def _do_start(self) -> None: ctx = mp.get_context("spawn") self._channels = [] self._processes = [] self._stdout_buffers = {} self._flush_events = {} self._shutdown_complete_events = {i: asyncio.Event() for i in range(self._num_processes)} + self._dead_processes = set() for process_idx in range(self._num_processes): - # Create channel pair parent_channel, child_queues = create_queue_pair(ctx) self._channels.append(parent_channel) - - # Initialize stdout buffer for this process self._stdout_buffers[process_idx] = [] - # Create and start subprocess proc = ctx.Process( target=_subprocess_main, args=( - child_queues[0], # send_q - child_queues[1], # recv_q - self._worker_fn, - self._threads_per_process, - process_idx, - self._threads_per_process, - self._redirect_output, - self._buffer_output, + child_queues[0], child_queues[1], + self._worker_fn, self._threads_per_process, + process_idx, self._threads_per_process, + self._redirect_output, self._buffer_output, self._output_flush_interval, ), ) proc.start() self._processes.append(proc) - self._running = True - self._dead_processes = set() - self._monitor_task = asyncio.create_task(self._monitor_processes()) - - async def _monitor_processes(self) -> None: - """Background task to detect dead subprocesses.""" - while self._running: - for proc_idx, proc in enumerate(self._processes): - if proc_idx not in self._dead_processes and proc.exitcode is not None: - # Process died - self._dead_processes.add(proc_idx) - exit_info = { - "exit_code": proc.exitcode, - "reason": f"Process exited with code {proc.exitcode}", - } - # Signal death for all workers in this process - for thread_idx in range(self._threads_per_process): - worker_id = proc_idx * self._threads_per_process + thread_idx - await self._recv_queue.put(WorkerMessage( - worker_id=worker_id, - key=POOL_UP_ERROR_CRASHED, - data=exit_info - )) - # Close the channel to unblock any recv_loop waiting on it - # This ensures the recv task exits rather than hanging forever - try: - await self._channels[proc_idx].close() - except Exception: - pass - # Set shutdown_complete_event so close() doesn't wait for dead processes - if proc_idx in self._shutdown_complete_events: - self._shutdown_complete_events[proc_idx].set() - await asyncio.sleep(0.5) # Check every 500ms - async def close(self, timeout: float | None = None) -> None: """Shut down all processes and clean up resources. - This method ensures that all pending output from subprocesses is received - before shutting down. It works by: - 1. Sending RPC_KEY_SHUTDOWN to each subprocess - 2. Waiting for each subprocess to send SHUTDOWN_COMPLETE (with final flush) - 3. Then closing channels and cleaning up - - Args: - timeout: Max seconds to wait for each process to finish gracefully. - If None, wait indefinitely. If timeout expires, processes - are forcefully terminated. + Overrides BasePool.close() because MultiprocessPool needs to: + 1. Start recv tasks BEFORE setting _running=False (to receive SHUTDOWN_COMPLETE) + 2. Send MP_DOWN_SHUTDOWN and wait for SHUTDOWN_COMPLETE + 3. Close channels, then cancel recv tasks, then join processes """ if not self._running: return - - # Ensure recv tasks are running BEFORE we set _running = False - # This ensures we can receive SHUTDOWN_COMPLETE messages self._start_recv_tasks() - self._running = False - # Cancel monitor task - if self._monitor_task and not self._monitor_task.done(): + # Cancel monitor + if self._monitor_task is not None and not self._monitor_task.done(): self._monitor_task.cancel() try: await self._monitor_task except asyncio.CancelledError: pass + self._monitor_task = None - # Send shutdown signals to each subprocess using MP_DOWN_SHUTDOWN (NOT RPC_KEY_SHUTDOWN) - # MP_DOWN_SHUTDOWN allows the subprocess to complete its shutdown sequence - # (including final flush and SHUTDOWN_COMPLETE) before the channel is closed + # Send shutdown to each subprocess for channel in self._channels: try: await channel.send(MP_DOWN_SHUTDOWN, None) @@ -715,24 +626,23 @@ async def close(self, timeout: float | None = None) -> None: pass # Wait for SHUTDOWN_COMPLETE from all processes - # The recv loop will set the events when it receives SHUTDOWN_COMPLETE - shutdown_timeout = timeout if timeout is not None else 30.0 # Default 30s + shutdown_timeout = timeout if timeout is not None else 30.0 try: await asyncio.wait_for( asyncio.gather(*[ self._shutdown_complete_events[i].wait() for i in range(self._num_processes) ]), - timeout=shutdown_timeout + timeout=shutdown_timeout, ) except TimeoutError: - pass # Continue with cleanup even if some processes didn't respond + pass - # Now close channels - this is safe since we've received all messages + # Close channels for channel in self._channels: await channel.close() - # Cancel recv tasks (they should already be done since they break after SHUTDOWN_COMPLETE) + # Cancel recv tasks for task in self._recv_tasks: if not task.done(): task.cancel() @@ -740,8 +650,9 @@ async def close(self, timeout: float | None = None) -> None: await task except asyncio.CancelledError: pass + self._recv_tasks.clear() - # Wait for processes to finish + # Join/terminate processes for proc in self._processes: proc.join(timeout=timeout) if proc.is_alive(): @@ -750,248 +661,140 @@ async def close(self, timeout: float | None = None) -> None: self._channels = [] self._processes = [] self._recv_queue = asyncio.Queue() - self._recv_tasks = [] - self._monitor_task = None self._dead_processes = set() self._stdout_buffers = {} self._flush_events = {} self._shutdown_complete_events = {} - async def send(self, worker_id: WorkerId, key: str, data: Any) -> None: - """Send a message to a specific worker.""" - if not self._running: - raise PoolNotStarted("Pool has not been started") - - if worker_id < 0 or worker_id >= self._num_workers: - raise ValueError(f"worker_id {worker_id} out of range [0, {self._num_workers})") - - process_idx, thread_idx = self._worker_id_to_process_thread(worker_id) - await self._channels[process_idx].send(MP_DOWN_DISPATCH, (thread_idx, key, data)) - - def _start_recv_tasks(self) -> None: - """Start background tasks that forward messages to the queue.""" - if self._recv_tasks: - return + async def _do_close(self, timeout: float | None = None) -> None: + # Not used — close() is overridden entirely + pass - async def recv_loop(process_idx: int, channel: ProcessChannel): - try: - # Keep receiving until shutdown complete or channel closed - while True: - # Use a timeout to periodically check if process is still alive - # This handles cases where the subprocess hangs during startup - try: - key, data = await channel.recv(timeout=1.0) - except RecvTimeout: - # Check if monitor has already detected and handled this dead process - # This prevents race conditions where recv_loop might exit before - # the monitor has put a CRASHED message in the queue - if process_idx in self._dead_processes: - # Monitor already handled it - safe to exit + def _create_recv_loops(self) -> list: + loops = [] + for process_idx, channel in enumerate(self._channels): + async def recv_loop(_proc_idx=process_idx, _channel=channel): + try: + while True: + try: + key, data = await _channel.recv(timeout=1.0) + except RecvTimeout: + if _proc_idx in self._dead_processes: + break + continue + + if key == MP_UP_RESPONSE: + worker_id, msg_key, msg_data = data + await self._recv_queue.put( + WorkerMessage(worker_id=worker_id, key=msg_key, data=msg_data) + ) + elif key == MP_UP_SUBPROCESS_ERROR: + for thread_idx in range(self._threads_per_process): + worker_id = _proc_idx * self._threads_per_process + thread_idx + await self._recv_queue.put(WorkerMessage( + worker_id=worker_id, key=POOL_UP_ERROR_EXCEPTION, data=data, + )) + elif key == MP_UP_STDOUT_BUFFER: + self._stdout_buffers[_proc_idx].extend(data) + if self._on_output is not None and data: + self._on_output(_proc_idx, data) + if _proc_idx in self._flush_events: + self._flush_events[_proc_idx].set() + elif key == MP_UP_SHUTDOWN_COMPLETE: + if _proc_idx in self._shutdown_complete_events: + self._shutdown_complete_events[_proc_idx].set() break - # Otherwise continue waiting (monitor will detect death if process crashed) - continue - - if key == MP_UP_RESPONSE: - worker_id, msg_key, msg_data = data - msg = WorkerMessage(worker_id=worker_id, key=msg_key, data=msg_data) - await self._recv_queue.put(msg) - elif key == MP_UP_SUBPROCESS_ERROR: - # Subprocess reported a fatal error - propagate to all workers in this process - for thread_idx in range(self._threads_per_process): - worker_id = process_idx * self._threads_per_process + thread_idx - await self._recv_queue.put(WorkerMessage( - worker_id=worker_id, - key=POOL_UP_ERROR_EXCEPTION, - data=data # dict with type, message, traceback - )) - elif key == MP_UP_STDOUT_BUFFER: - # Append received buffer to our local buffer for this process - self._stdout_buffers[process_idx].extend(data) - # Call on_output callback if provided - if self._on_output is not None and data: - self._on_output(process_idx, data) - # Signal that flush response was received - if process_idx in self._flush_events: - self._flush_events[process_idx].set() - elif key == MP_UP_SHUTDOWN_COMPLETE: - # Signal that this subprocess has finished shutdown - if process_idx in self._shutdown_complete_events: - self._shutdown_complete_events[process_idx].set() - # Exit the loop after receiving shutdown complete - break - except (ChannelClosed, asyncio.CancelledError): - pass - except Exception as e: - logging.getLogger("netrun.pool").error( - f"recv_loop for process {process_idx} crashed: {e}", exc_info=True - ) - # Notify for all workers in this process - for worker_id in range( - process_idx * self._threads_per_process, - (process_idx + 1) * self._threads_per_process, - ): + except (ChannelClosed, asyncio.CancelledError): + pass + except Exception as e: + logging.getLogger("netrun.pool").error( + f"recv_loop for process {_proc_idx} crashed: {e}", exc_info=True + ) + for worker_id in range( + _proc_idx * self._threads_per_process, + (_proc_idx + 1) * self._threads_per_process, + ): + await self._recv_queue.put(WorkerMessage( + worker_id=worker_id, key=POOL_UP_ERROR_CRASHED, + data={"reason": f"recv_loop exception: {e}"}, + )) + loops.append(recv_loop()) + return loops + + async def _check_worker_health(self) -> None: + for proc_idx, proc in enumerate(self._processes): + if proc_idx not in self._dead_processes and proc.exitcode is not None: + self._dead_processes.add(proc_idx) + exit_info = { + "exit_code": proc.exitcode, + "reason": f"Process exited with code {proc.exitcode}", + } + for thread_idx in range(self._threads_per_process): + worker_id = proc_idx * self._threads_per_process + thread_idx await self._recv_queue.put(WorkerMessage( - worker_id=worker_id, - key=POOL_UP_ERROR_CRASHED, - data={"reason": f"recv_loop exception: {e}"} + worker_id=worker_id, key=POOL_UP_ERROR_CRASHED, data=exit_info, )) + try: + await self._channels[proc_idx].close() + except Exception: + pass + if proc_idx in self._shutdown_complete_events: + self._shutdown_complete_events[proc_idx].set() - for process_idx, channel in enumerate(self._channels): - task = asyncio.create_task(recv_loop(process_idx, channel)) - self._recv_tasks.append(task) - - async def recv(self, timeout: float | None = None) -> WorkerMessage: - """Receive a message from any worker. - - Raises: - WorkerException: If the worker raised an exception - WorkerCrashed: If the worker died unexpectedly - WorkerTimeout: If the worker timed out - RecvTimeout: If this recv() call times out - """ - if not self._running: - raise PoolNotStarted("Pool has not been started") - - self._start_recv_tasks() - - try: - if timeout is None: - msg = await self._recv_queue.get() - else: - msg = await asyncio.wait_for( - self._recv_queue.get(), - timeout=timeout, - ) - except TimeoutError: - raise RecvTimeout(f"Receive timed out after {timeout}s") - - _check_error_and_raise(msg) - return msg - - async def try_recv(self) -> WorkerMessage | None: - """Non-blocking receive from any worker. - - Raises: - WorkerException: If the worker raised an exception - WorkerCrashed: If the worker died unexpectedly - WorkerTimeout: If the worker timed out - """ - if not self._running: - raise PoolNotStarted("Pool has not been started") - - # If recv tasks are running, check the queue first - if self._recv_tasks: - try: - msg = self._recv_queue.get_nowait() - _check_error_and_raise(msg) - return msg - except asyncio.QueueEmpty: - return None - - # Otherwise, read directly from channels + async def _try_recv_direct(self) -> WorkerMessage | None: + """Direct non-blocking receive (when recv tasks aren't running).""" for process_idx, channel in enumerate(self._channels): result = await channel.try_recv() if result is not None: key, data = result if key == MP_UP_RESPONSE: worker_id, msg_key, msg_data = data - msg = WorkerMessage(worker_id=worker_id, key=msg_key, data=msg_data) - _check_error_and_raise(msg) - return msg - + return WorkerMessage(worker_id=worker_id, key=msg_key, data=msg_data) return None - async def broadcast(self, key: str, data: Any) -> None: - """Send a message to all workers.""" + async def send(self, worker_id: WorkerId, key: str, data: Any) -> None: if not self._running: raise PoolNotStarted("Pool has not been started") + if worker_id < 0 or worker_id >= self._num_workers: + raise ValueError(f"worker_id {worker_id} out of range [0, {self._num_workers})") + process_idx, thread_idx = self._worker_id_to_process_thread(worker_id) + await self._channels[process_idx].send(MP_DOWN_DISPATCH, (thread_idx, key, data)) + async def broadcast(self, key: str, data: Any) -> None: + if not self._running: + raise PoolNotStarted("Pool has not been started") for channel in self._channels: await channel.send(MP_DOWN_BROADCAST, (key, data)) - async def flush_stdout(self, process_idx: int, timeout: float|None = None) -> OutputBuffer: - """Flush and retrieve stdout/stderr buffer from a specific process. - - Sends a flush request to the subprocess, waits for the response, - and returns the buffer contents. The subprocess buffer is cleared. - - Args: - process_idx: Index of the process (0 to num_processes-1) - timeout: Maximum time to wait for response in seconds - - Returns: - List of (timestamp, is_stdout, text) tuples. - is_stdout is True for stdout, False for stderr. - - Raises: - PoolNotStarted: If the pool is not running - ValueError: If process_idx is out of range - RecvTimeout: If the subprocess doesn't respond in time - """ + async def flush_stdout(self, process_idx: int, timeout: float | None = None) -> OutputBuffer: + """Flush and retrieve stdout/stderr buffer from a specific process.""" if not self._running: raise PoolNotStarted("Pool has not been started") - if process_idx < 0 or process_idx >= self._num_processes: raise ValueError(f"process_idx {process_idx} out of range [0, {self._num_processes})") - # Ensure recv tasks are running to capture the response self._start_recv_tasks() - - # Create an event to wait for the flush response event = asyncio.Event() self._flush_events[process_idx] = event - - # Send flush request await self._channels[process_idx].send(MP_DOWN_FLUSH_STDOUT, None) - - # Wait for response try: await asyncio.wait_for(event.wait(), timeout=timeout) except TimeoutError: raise RecvTimeout(f"flush_stdout timed out after {timeout}s") finally: - # Clean up event self._flush_events.pop(process_idx, None) - - # Extract buffer and clear it result = self._stdout_buffers[process_idx] self._stdout_buffers[process_idx] = [] return result - async def flush_all_stdout(self, timeout: float|None = None) -> dict[int, OutputBuffer]: - """Flush and retrieve stdout/stderr buffers from all processes. - - Args: - timeout: Maximum time to wait for each process response - - Returns: - Dictionary mapping process_idx to buffer contents. - Each buffer is a list of (timestamp, is_stdout, text) tuples. - - Raises: - PoolNotStarted: If the pool is not running - """ + async def flush_all_stdout(self, timeout: float | None = None) -> dict[int, OutputBuffer]: + """Flush and retrieve stdout/stderr buffers from all processes.""" if not self._running: raise PoolNotStarted("Pool has not been started") - - # Flush all processes concurrently - tasks = [ - self.flush_stdout(process_idx, timeout=timeout) - for process_idx in range(self._num_processes) - ] + tasks = [self.flush_stdout(i, timeout=timeout) for i in range(self._num_processes)] results = await asyncio.gather(*tasks) - return {i: result for i, result in enumerate(results)} - async def __aenter__(self) -> "MultiprocessPool": - """Context manager entry - starts the pool.""" - await self.start() - return self - - async def __aexit__(self, exc_type, exc_val, exc_tb) -> None: - """Context manager exit - closes the pool.""" - await self.close() - # %% [markdown] # ## Example # diff --git a/netrun/pts/netrun/04_pool/04_aio.pct.py b/netrun/pts/netrun/04_pool/04_aio.pct.py index 6248df84..6245f81f 100644 --- a/netrun/pts/netrun/04_pool/04_aio.pct.py +++ b/netrun/pts/netrun/04_pool/04_aio.pct.py @@ -55,6 +55,7 @@ create_async_channel_pair, ) from netrun.pool.base import ( + BasePool, WorkerId, WorkerMessage, PoolNotStarted, @@ -77,7 +78,7 @@ # %% #|export -class SingleWorkerPool: +class SingleWorkerPool(BasePool): """A pool with a single async worker coroutine. Designed for the main thread of netrun where the "worker" is @@ -85,67 +86,22 @@ class SingleWorkerPool: or MultiprocessPool, this does not spawn threads or processes. """ - def __init__( - self, - worker_fn: AsyncWorkerFn, - ): + def __init__(self, worker_fn: AsyncWorkerFn, **kwargs): """Create a single-worker async pool. Args: worker_fn: Async function to run as the worker. Signature: async def worker(channel: AsyncChannel, worker_id: int) -> None """ + super().__init__(num_workers=1, **kwargs) self._worker_fn = worker_fn - self._running = False - - # Will be populated on start() self._channel: AsyncChannel | None = None self._worker_channel: AsyncChannel | None = None self._worker_task: asyncio.Task | None = None - self._recv_queue: asyncio.Queue = asyncio.Queue() - self._recv_task: asyncio.Task | None = None - self._monitor_task: asyncio.Task | None = None - - @property - def num_workers(self) -> int: - """Total number of workers in the pool. Always 1.""" - return 1 - - @property - def is_running(self) -> bool: - """Whether the pool has been started.""" - return self._running - - async def start(self) -> None: - """Start the worker.""" - if self._running: - raise PoolAlreadyStarted("Pool is already running") + async def _do_start(self) -> None: self._channel, self._worker_channel = create_async_channel_pair() - - # Start worker as an async task - self._worker_task = asyncio.create_task( - self._run_worker() - ) - self._running = True - self._monitor_task = asyncio.create_task(self._monitor_worker()) - - async def _monitor_worker(self) -> None: - """Detect if worker task dies unexpectedly.""" - try: - await self._worker_task - except asyncio.CancelledError: - return - except Exception: - pass - - if self._running: - # Task ended while pool still running - await self._recv_queue.put(WorkerMessage( - worker_id=0, - key=POOL_UP_ERROR_CRASHED, - data={"reason": "Worker task ended unexpectedly"} - )) + self._worker_task = asyncio.create_task(self._run_worker()) async def _run_worker(self) -> None: """Run the worker function.""" @@ -154,45 +110,16 @@ async def _run_worker(self) -> None: except ChannelClosed: pass except Exception as e: - # Try to send exception object back (no serialization needed for async pool) try: await self._worker_channel.send(POOL_UP_ERROR_EXCEPTION, e) except Exception: pass - async def close(self, timeout: float | None = None) -> None: - """Shut down the worker and clean up resources. - - Args: - timeout: Max seconds to wait for the worker task to finish. - If None, wait indefinitely. Note: asyncio tasks are - cancelled, so timeout mainly affects graceful shutdown. - """ - if not self._running: - return - - self._running = False - - # Cancel monitor task first - if self._monitor_task and not self._monitor_task.done(): - self._monitor_task.cancel() - try: - await self._monitor_task - except asyncio.CancelledError: - pass - - # Close the channel to signal worker to stop + async def _do_close(self, timeout: float | None = None) -> None: + # Close channel to signal worker to stop if self._channel: await self._channel.close() - # Cancel recv task if running - if self._recv_task and not self._recv_task.done(): - self._recv_task.cancel() - try: - await self._recv_task - except asyncio.CancelledError: - pass - # Wait for worker task to finish if self._worker_task and not self._worker_task.done(): if timeout is not None: @@ -205,7 +132,6 @@ async def close(self, timeout: float | None = None) -> None: except asyncio.CancelledError: pass else: - # Wait indefinitely for graceful shutdown try: await self._worker_task except asyncio.CancelledError: @@ -214,31 +140,13 @@ async def close(self, timeout: float | None = None) -> None: self._channel = None self._worker_channel = None self._worker_task = None - self._recv_queue = asyncio.Queue() - self._recv_task = None - self._monitor_task = None - - async def send(self, worker_id: WorkerId, key: str, data: Any) -> None: - """Send a message to the worker.""" - if not self._running: - raise PoolNotStarted("Pool has not been started") - - if worker_id != 0: - raise ValueError(f"worker_id must be 0, got {worker_id}") - - await self._channel.send(key, data) - - def _start_recv_task(self) -> None: - """Start background task that forwards messages to the queue.""" - if self._recv_task is not None: - return + def _create_recv_loops(self) -> list: async def recv_loop(): try: while self._running: key, data = await self._channel.recv() - msg = WorkerMessage(worker_id=0, key=key, data=data) - await self._recv_queue.put(msg) + await self._recv_queue.put(WorkerMessage(worker_id=0, key=key, data=data)) except (ChannelClosed, asyncio.CancelledError): pass except Exception as e: @@ -246,86 +154,37 @@ async def recv_loop(): f"recv_loop for worker 0 crashed: {e}", exc_info=True ) await self._recv_queue.put(WorkerMessage( - worker_id=0, - key=POOL_UP_ERROR_CRASHED, - data={"reason": f"recv_loop exception: {e}"} + worker_id=0, key=POOL_UP_ERROR_CRASHED, + data={"reason": f"recv_loop exception: {e}"}, )) + return [recv_loop()] - self._recv_task = asyncio.create_task(recv_loop()) - - async def recv(self, timeout: float | None = None) -> WorkerMessage: - """Receive a message from the worker. - - Raises: - WorkerException: If the worker raised an exception - WorkerCrashed: If the worker died unexpectedly - WorkerTimeout: If the worker timed out - RecvTimeout: If this recv() call times out - """ - if not self._running: - raise PoolNotStarted("Pool has not been started") - - self._start_recv_task() - - try: - if timeout is None: - msg = await self._recv_queue.get() - else: - msg = await asyncio.wait_for( - self._recv_queue.get(), - timeout=timeout, - ) - except TimeoutError: - raise RecvTimeout(f"Receive timed out after {timeout}s") - - _check_error_and_raise(msg) - return msg - - async def try_recv(self) -> WorkerMessage | None: - """Non-blocking receive from the worker. - - Raises: - WorkerException: If the worker raised an exception - WorkerCrashed: If the worker died unexpectedly - WorkerTimeout: If the worker timed out - """ - if not self._running: - raise PoolNotStarted("Pool has not been started") - - # If recv task is running, check the queue first - if self._recv_task is not None: - try: - msg = self._recv_queue.get_nowait() - _check_error_and_raise(msg) - return msg - except asyncio.QueueEmpty: - return None + async def _check_worker_health(self) -> None: + if self._worker_task and self._worker_task.done() and self._running: + await self._recv_queue.put(WorkerMessage( + worker_id=0, key=POOL_UP_ERROR_CRASHED, + data={"reason": "Worker task ended unexpectedly"}, + )) - # Otherwise, read directly from channel + async def _try_recv_direct(self) -> WorkerMessage | None: + """Read directly from channel (when recv tasks aren't running).""" result = await self._channel.try_recv() if result is not None: key, data = result - msg = WorkerMessage(worker_id=0, key=key, data=data) - _check_error_and_raise(msg) - return msg - + return WorkerMessage(worker_id=0, key=key, data=data) return None - async def broadcast(self, key: str, data: Any) -> None: - """Send a message to the worker (same as send for single worker).""" + async def send(self, worker_id: WorkerId, key: str, data: Any) -> None: if not self._running: raise PoolNotStarted("Pool has not been started") - + if worker_id != 0: + raise ValueError(f"worker_id must be 0, got {worker_id}") await self._channel.send(key, data) - async def __aenter__(self) -> "SingleWorkerPool": - """Context manager entry - starts the pool.""" - await self.start() - return self - - async def __aexit__(self, exc_type, exc_val, exc_tb) -> None: - """Context manager exit - closes the pool.""" - await self.close() + async def broadcast(self, key: str, data: Any) -> None: + if not self._running: + raise PoolNotStarted("Pool has not been started") + await self._channel.send(key, data) # %% [markdown] # ## Example: Echo Worker diff --git a/netrun/pts/netrun/05_execution_manager.pct.py b/netrun/pts/netrun/05_execution_manager.pct.py index a65820fc..3bddac3e 100644 --- a/netrun/pts/netrun/05_execution_manager.pct.py +++ b/netrun/pts/netrun/05_execution_manager.pct.py @@ -249,13 +249,6 @@ def _worker_func( else: raise ValueError(f"Unknown execution manager protocol key: '{key}'.") -def _thread_worker_func(channel, worker_id, func_preprocessor: Callable | None = None, func_done_callback: Callable | None = None, shared_loop: asyncio.AbstractEventLoop | None = None): - return _worker_func(is_in_main_process=True, channel=channel, worker_id=worker_id, func_preprocessor=func_preprocessor, func_done_callback=func_done_callback, shared_loop=shared_loop) - -# If the worker is in a multiprocess pool, then the result needs to be pickleable for it to be sent back without being converted as `str(result)`. -def _multiprocess_worker_func(channel, worker_id, func_preprocessor: Callable | None = None, func_done_callback: Callable | None = None, shared_loop: asyncio.AbstractEventLoop | None = None): - return _worker_func(is_in_main_process=False, channel=channel, worker_id=worker_id, func_preprocessor=func_preprocessor, func_done_callback=func_done_callback, shared_loop=shared_loop) - # %% #|exporti async def _async_worker_func( @@ -499,7 +492,7 @@ def __init__(self, pool_configs: dict[str, tuple[type[PoolType], dict[str, Any]] self._started = False self._worker_jobs: dict[tuple[str, str], list[SubmittedJobInfo]] = {} # (pool_id, worker_id) -> list of SubmittedJobInfo - self._worker_round_robin_lst: list[tuple[str, str]] = [] + self._round_robin_counter: int = 0 # Shared event loop for async function dispatch in thread/multiprocess workers. # Created in start(), cleaned up in close(). @@ -533,12 +526,12 @@ async def start(self) -> None: func_done_callback = pool_kwargs.pop('func_done_callback', None) if pool_type == ThreadPool: - worker_fn = functools.partial(_thread_worker_func, func_preprocessor=func_preprocessor, func_done_callback=func_done_callback, shared_loop=self._shared_loop) + worker_fn = functools.partial(_worker_func, True, func_preprocessor=func_preprocessor, func_done_callback=func_done_callback, shared_loop=self._shared_loop) self._pools[pool_id] = ThreadPool(**pool_kwargs, worker_fn=worker_fn) elif pool_type == MultiprocessPool: # Multiprocess workers create their own process-local shared loop # (event loops can't cross process boundaries) - worker_fn = functools.partial(_multiprocess_worker_func, func_preprocessor=func_preprocessor, func_done_callback=func_done_callback) + worker_fn = functools.partial(_worker_func, False, func_preprocessor=func_preprocessor, func_done_callback=func_done_callback) self._pools[pool_id] = MultiprocessPool(**pool_kwargs, worker_fn=worker_fn) elif pool_type == RemotePoolClient: self._pools[pool_id] = RemotePoolClient(**pool_kwargs) @@ -684,9 +677,6 @@ async def run( worker_id=worker_id, ) self._worker_jobs[(pool_id, worker_id)].append(job_info) - if (pool_id, worker_id) in self._worker_round_robin_lst: - self._worker_round_robin_lst.remove((pool_id, worker_id)) - self._worker_round_robin_lst.append((pool_id, worker_id)) msg_id = None try: @@ -754,12 +744,9 @@ async def run_allocate( # Select worker based on allocation method if allocation_method == RunAllocationMethod.ROUND_ROBIN: - round_robin_lst = [p for p in self._worker_round_robin_lst if p in worker_ids] - not_in_round_robin_lst = [p for p in worker_ids if p not in round_robin_lst] - if not_in_round_robin_lst: - pool_id, worker_id = not_in_round_robin_lst[0] - else: - pool_id, worker_id = round_robin_lst[0] + idx = self._round_robin_counter % len(worker_ids) + pool_id, worker_id = worker_ids[idx] + self._round_robin_counter += 1 elif allocation_method == RunAllocationMethod.RANDOM: pool_id, worker_id = random.choice(worker_ids) diff --git a/netrun/src/netrun/execution_manager.py b/netrun/src/netrun/execution_manager.py index 3cd31024..3bdf29be 100644 --- a/netrun/src/netrun/execution_manager.py +++ b/netrun/src/netrun/execution_manager.py @@ -212,13 +212,6 @@ def _worker_func( else: raise ValueError(f"Unknown execution manager protocol key: '{key}'.") -def _thread_worker_func(channel, worker_id, func_preprocessor: Callable | None = None, func_done_callback: Callable | None = None, shared_loop: asyncio.AbstractEventLoop | None = None): - return _worker_func(is_in_main_process=True, channel=channel, worker_id=worker_id, func_preprocessor=func_preprocessor, func_done_callback=func_done_callback, shared_loop=shared_loop) - -# If the worker is in a multiprocess pool, then the result needs to be pickleable for it to be sent back without being converted as `str(result)`. -def _multiprocess_worker_func(channel, worker_id, func_preprocessor: Callable | None = None, func_done_callback: Callable | None = None, shared_loop: asyncio.AbstractEventLoop | None = None): - return _worker_func(is_in_main_process=False, channel=channel, worker_id=worker_id, func_preprocessor=func_preprocessor, func_done_callback=func_done_callback, shared_loop=shared_loop) - # %% pts/netrun/05_execution_manager.pct.py 12 async def _async_worker_func( channel, @@ -448,7 +441,7 @@ def __init__(self, pool_configs: dict[str, tuple[type[PoolType], dict[str, Any]] self._started = False self._worker_jobs: dict[tuple[str, str], list[SubmittedJobInfo]] = {} # (pool_id, worker_id) -> list of SubmittedJobInfo - self._worker_round_robin_lst: list[tuple[str, str]] = [] + self._round_robin_counter: int = 0 # Shared event loop for async function dispatch in thread/multiprocess workers. # Created in start(), cleaned up in close(). @@ -482,12 +475,12 @@ async def start(self) -> None: func_done_callback = pool_kwargs.pop('func_done_callback', None) if pool_type == ThreadPool: - worker_fn = functools.partial(_thread_worker_func, func_preprocessor=func_preprocessor, func_done_callback=func_done_callback, shared_loop=self._shared_loop) + worker_fn = functools.partial(_worker_func, True, func_preprocessor=func_preprocessor, func_done_callback=func_done_callback, shared_loop=self._shared_loop) self._pools[pool_id] = ThreadPool(**pool_kwargs, worker_fn=worker_fn) elif pool_type == MultiprocessPool: # Multiprocess workers create their own process-local shared loop # (event loops can't cross process boundaries) - worker_fn = functools.partial(_multiprocess_worker_func, func_preprocessor=func_preprocessor, func_done_callback=func_done_callback) + worker_fn = functools.partial(_worker_func, False, func_preprocessor=func_preprocessor, func_done_callback=func_done_callback) self._pools[pool_id] = MultiprocessPool(**pool_kwargs, worker_fn=worker_fn) elif pool_type == RemotePoolClient: self._pools[pool_id] = RemotePoolClient(**pool_kwargs) @@ -633,9 +626,6 @@ async def run( worker_id=worker_id, ) self._worker_jobs[(pool_id, worker_id)].append(job_info) - if (pool_id, worker_id) in self._worker_round_robin_lst: - self._worker_round_robin_lst.remove((pool_id, worker_id)) - self._worker_round_robin_lst.append((pool_id, worker_id)) msg_id = None try: @@ -703,12 +693,9 @@ async def run_allocate( # Select worker based on allocation method if allocation_method == RunAllocationMethod.ROUND_ROBIN: - round_robin_lst = [p for p in self._worker_round_robin_lst if p in worker_ids] - not_in_round_robin_lst = [p for p in worker_ids if p not in round_robin_lst] - if not_in_round_robin_lst: - pool_id, worker_id = not_in_round_robin_lst[0] - else: - pool_id, worker_id = round_robin_lst[0] + idx = self._round_robin_counter % len(worker_ids) + pool_id, worker_id = worker_ids[idx] + self._round_robin_counter += 1 elif allocation_method == RunAllocationMethod.RANDOM: pool_id, worker_id = random.choice(worker_ids) diff --git a/netrun/src/netrun/pool/aio.py b/netrun/src/netrun/pool/aio.py index 3a2e3588..d5447935 100644 --- a/netrun/src/netrun/pool/aio.py +++ b/netrun/src/netrun/pool/aio.py @@ -14,6 +14,7 @@ create_async_channel_pair, ) from ..pool.base import ( + BasePool, WorkerId, WorkerMessage, PoolNotStarted, @@ -28,7 +29,7 @@ """Type for async worker functions: async def worker(channel, worker_id) -> None""" # %% pts/netrun/04_pool/04_aio.pct.py 7 -class SingleWorkerPool: +class SingleWorkerPool(BasePool): """A pool with a single async worker coroutine. Designed for the main thread of netrun where the "worker" is @@ -36,67 +37,22 @@ class SingleWorkerPool: or MultiprocessPool, this does not spawn threads or processes. """ - def __init__( - self, - worker_fn: AsyncWorkerFn, - ): + def __init__(self, worker_fn: AsyncWorkerFn, **kwargs): """Create a single-worker async pool. Args: worker_fn: Async function to run as the worker. Signature: async def worker(channel: AsyncChannel, worker_id: int) -> None """ + super().__init__(num_workers=1, **kwargs) self._worker_fn = worker_fn - self._running = False - - # Will be populated on start() self._channel: AsyncChannel | None = None self._worker_channel: AsyncChannel | None = None self._worker_task: asyncio.Task | None = None - self._recv_queue: asyncio.Queue = asyncio.Queue() - self._recv_task: asyncio.Task | None = None - self._monitor_task: asyncio.Task | None = None - - @property - def num_workers(self) -> int: - """Total number of workers in the pool. Always 1.""" - return 1 - - @property - def is_running(self) -> bool: - """Whether the pool has been started.""" - return self._running - - async def start(self) -> None: - """Start the worker.""" - if self._running: - raise PoolAlreadyStarted("Pool is already running") + async def _do_start(self) -> None: self._channel, self._worker_channel = create_async_channel_pair() - - # Start worker as an async task - self._worker_task = asyncio.create_task( - self._run_worker() - ) - self._running = True - self._monitor_task = asyncio.create_task(self._monitor_worker()) - - async def _monitor_worker(self) -> None: - """Detect if worker task dies unexpectedly.""" - try: - await self._worker_task - except asyncio.CancelledError: - return - except Exception: - pass - - if self._running: - # Task ended while pool still running - await self._recv_queue.put(WorkerMessage( - worker_id=0, - key=POOL_UP_ERROR_CRASHED, - data={"reason": "Worker task ended unexpectedly"} - )) + self._worker_task = asyncio.create_task(self._run_worker()) async def _run_worker(self) -> None: """Run the worker function.""" @@ -105,45 +61,16 @@ async def _run_worker(self) -> None: except ChannelClosed: pass except Exception as e: - # Try to send exception object back (no serialization needed for async pool) try: await self._worker_channel.send(POOL_UP_ERROR_EXCEPTION, e) except Exception: pass - async def close(self, timeout: float | None = None) -> None: - """Shut down the worker and clean up resources. - - Args: - timeout: Max seconds to wait for the worker task to finish. - If None, wait indefinitely. Note: asyncio tasks are - cancelled, so timeout mainly affects graceful shutdown. - """ - if not self._running: - return - - self._running = False - - # Cancel monitor task first - if self._monitor_task and not self._monitor_task.done(): - self._monitor_task.cancel() - try: - await self._monitor_task - except asyncio.CancelledError: - pass - - # Close the channel to signal worker to stop + async def _do_close(self, timeout: float | None = None) -> None: + # Close channel to signal worker to stop if self._channel: await self._channel.close() - # Cancel recv task if running - if self._recv_task and not self._recv_task.done(): - self._recv_task.cancel() - try: - await self._recv_task - except asyncio.CancelledError: - pass - # Wait for worker task to finish if self._worker_task and not self._worker_task.done(): if timeout is not None: @@ -156,7 +83,6 @@ async def close(self, timeout: float | None = None) -> None: except asyncio.CancelledError: pass else: - # Wait indefinitely for graceful shutdown try: await self._worker_task except asyncio.CancelledError: @@ -165,31 +91,13 @@ async def close(self, timeout: float | None = None) -> None: self._channel = None self._worker_channel = None self._worker_task = None - self._recv_queue = asyncio.Queue() - self._recv_task = None - self._monitor_task = None - - async def send(self, worker_id: WorkerId, key: str, data: Any) -> None: - """Send a message to the worker.""" - if not self._running: - raise PoolNotStarted("Pool has not been started") - - if worker_id != 0: - raise ValueError(f"worker_id must be 0, got {worker_id}") - - await self._channel.send(key, data) - - def _start_recv_task(self) -> None: - """Start background task that forwards messages to the queue.""" - if self._recv_task is not None: - return + def _create_recv_loops(self) -> list: async def recv_loop(): try: while self._running: key, data = await self._channel.recv() - msg = WorkerMessage(worker_id=0, key=key, data=data) - await self._recv_queue.put(msg) + await self._recv_queue.put(WorkerMessage(worker_id=0, key=key, data=data)) except (ChannelClosed, asyncio.CancelledError): pass except Exception as e: @@ -197,83 +105,34 @@ async def recv_loop(): f"recv_loop for worker 0 crashed: {e}", exc_info=True ) await self._recv_queue.put(WorkerMessage( - worker_id=0, - key=POOL_UP_ERROR_CRASHED, - data={"reason": f"recv_loop exception: {e}"} + worker_id=0, key=POOL_UP_ERROR_CRASHED, + data={"reason": f"recv_loop exception: {e}"}, )) + return [recv_loop()] - self._recv_task = asyncio.create_task(recv_loop()) - - async def recv(self, timeout: float | None = None) -> WorkerMessage: - """Receive a message from the worker. - - Raises: - WorkerException: If the worker raised an exception - WorkerCrashed: If the worker died unexpectedly - WorkerTimeout: If the worker timed out - RecvTimeout: If this recv() call times out - """ - if not self._running: - raise PoolNotStarted("Pool has not been started") - - self._start_recv_task() - - try: - if timeout is None: - msg = await self._recv_queue.get() - else: - msg = await asyncio.wait_for( - self._recv_queue.get(), - timeout=timeout, - ) - except TimeoutError: - raise RecvTimeout(f"Receive timed out after {timeout}s") - - _check_error_and_raise(msg) - return msg - - async def try_recv(self) -> WorkerMessage | None: - """Non-blocking receive from the worker. - - Raises: - WorkerException: If the worker raised an exception - WorkerCrashed: If the worker died unexpectedly - WorkerTimeout: If the worker timed out - """ - if not self._running: - raise PoolNotStarted("Pool has not been started") - - # If recv task is running, check the queue first - if self._recv_task is not None: - try: - msg = self._recv_queue.get_nowait() - _check_error_and_raise(msg) - return msg - except asyncio.QueueEmpty: - return None + async def _check_worker_health(self) -> None: + if self._worker_task and self._worker_task.done() and self._running: + await self._recv_queue.put(WorkerMessage( + worker_id=0, key=POOL_UP_ERROR_CRASHED, + data={"reason": "Worker task ended unexpectedly"}, + )) - # Otherwise, read directly from channel + async def _try_recv_direct(self) -> WorkerMessage | None: + """Read directly from channel (when recv tasks aren't running).""" result = await self._channel.try_recv() if result is not None: key, data = result - msg = WorkerMessage(worker_id=0, key=key, data=data) - _check_error_and_raise(msg) - return msg - + return WorkerMessage(worker_id=0, key=key, data=data) return None - async def broadcast(self, key: str, data: Any) -> None: - """Send a message to the worker (same as send for single worker).""" + async def send(self, worker_id: WorkerId, key: str, data: Any) -> None: if not self._running: raise PoolNotStarted("Pool has not been started") - + if worker_id != 0: + raise ValueError(f"worker_id must be 0, got {worker_id}") await self._channel.send(key, data) - async def __aenter__(self) -> "SingleWorkerPool": - """Context manager entry - starts the pool.""" - await self.start() - return self - - async def __aexit__(self, exc_type, exc_val, exc_tb) -> None: - """Context manager exit - closes the pool.""" - await self.close() + async def broadcast(self, key: str, data: Any) -> None: + if not self._running: + raise PoolNotStarted("Pool has not been started") + await self._channel.send(key, data) diff --git a/netrun/src/netrun/pool/base.py b/netrun/src/netrun/pool/base.py index 5f54f414..fd462775 100644 --- a/netrun/src/netrun/pool/base.py +++ b/netrun/src/netrun/pool/base.py @@ -1,6 +1,6 @@ # AUTOGENERATED! DO NOT EDIT! File to edit: pts/netrun/04_pool/00_base.pct.py -__all__ = ['POOL_UP_ERROR_CRASHED', 'POOL_UP_ERROR_EXCEPTION', 'POOL_UP_ERROR_KEYS', 'POOL_UP_ERROR_TIMEOUT', 'Pool', 'PoolAlreadyStarted', 'PoolError', 'PoolNotStarted', 'WorkerCrashed', 'WorkerError', 'WorkerException', 'WorkerFn', 'WorkerId', 'WorkerMessage', 'WorkerTimeout'] +__all__ = ['BasePool', 'POOL_UP_ERROR_CRASHED', 'POOL_UP_ERROR_EXCEPTION', 'POOL_UP_ERROR_KEYS', 'POOL_UP_ERROR_TIMEOUT', 'Pool', 'PoolAlreadyStarted', 'PoolError', 'PoolNotStarted', 'WorkerCrashed', 'WorkerError', 'WorkerException', 'WorkerFn', 'WorkerId', 'WorkerMessage', 'WorkerTimeout'] # %% pts/netrun/04_pool/00_base.pct.py 3 from typing import Any, Protocol, runtime_checkable @@ -203,3 +203,226 @@ def _check_error_and_raise(msg: WorkerMessage) -> None: raise WorkerCrashed(msg.worker_id, msg.data) elif msg.key == POOL_UP_ERROR_TIMEOUT: raise WorkerTimeout(msg.worker_id, msg.data["timeout_seconds"]) + +# %% pts/netrun/04_pool/00_base.pct.py 25 +import asyncio +import time +import logging +from abc import ABC, abstractmethod +from collections.abc import Coroutine + +class BasePool(ABC): + """Abstract base class for worker pools. + + Provides concrete implementations of recv(), try_recv(), start(), close(), + and worker health monitoring with optional silence detection. Subclasses + implement the pool-specific worker lifecycle and message routing. + + Subclass contract: + _do_start() — Create workers and channels + _do_close(timeout) — Shut down workers, clean up resources + _create_recv_loops() — Return coroutines that forward messages to _recv_queue + _check_worker_health() — Detect dead workers, put errors on _recv_queue + send(worker_id, ...) — Send to a specific worker + broadcast(key, data) — Send to all workers + """ + + def __init__(self, num_workers: int, *, silence_timeout: float | None = None): + """Initialize the base pool. + + Args: + num_workers: Number of workers in this pool. + silence_timeout: If set, log a warning when any worker has been + silent for this many seconds. None disables silence detection. + """ + self._num_workers = num_workers + self._running = False + self._recv_queue: asyncio.Queue[WorkerMessage] = asyncio.Queue() + self._recv_tasks: list[asyncio.Task] = [] + self._monitor_task: asyncio.Task | None = None + self._silence_timeout = silence_timeout + self._last_message_time: dict[int, float] = {} + + @property + def num_workers(self) -> int: + return self._num_workers + + @property + def is_running(self) -> bool: + return self._running + + async def start(self) -> None: + """Start the pool: create workers, begin health monitoring.""" + if self._running: + raise PoolAlreadyStarted("Pool is already running") + await self._do_start() + self._running = True + self._monitor_task = asyncio.create_task(self._monitor_loop()) + + async def close(self, timeout: float | None = None) -> None: + """Shut down the pool: stop workers, cancel tasks, clean up.""" + if not self._running: + return + # Ensure recv tasks are running before setting _running=False. + # MultiprocessPool needs recv tasks to receive SHUTDOWN_COMPLETE. + self._start_recv_tasks() + self._running = False + # Cancel monitor + if self._monitor_task is not None and not self._monitor_task.done(): + self._monitor_task.cancel() + try: + await self._monitor_task + except asyncio.CancelledError: + pass + self._monitor_task = None + # Subclass shutdown (send shutdown signals, wait, close channels, join) + await self._do_close(timeout) + # Cancel recv tasks + for task in self._recv_tasks: + if not task.done(): + task.cancel() + try: + await task + except asyncio.CancelledError: + pass + self._recv_tasks.clear() + self._recv_queue = asyncio.Queue() + + def _start_recv_tasks(self) -> None: + """Start background tasks that forward worker messages to _recv_queue. + + Idempotent — only creates tasks on first call. + """ + if self._recv_tasks: + return + for coro in self._create_recv_loops(): + self._recv_tasks.append(asyncio.create_task(coro)) + + async def recv(self, timeout: float | None = None) -> WorkerMessage: + """Receive a message from any worker. + + Raises: + PoolNotStarted: If pool hasn't been started + RecvTimeout: If timeout expires + WorkerException: If a worker raised an exception + WorkerCrashed: If a worker died unexpectedly + WorkerTimeout: If a worker timed out + """ + if not self._running: + raise PoolNotStarted("Pool has not been started") + self._start_recv_tasks() + try: + if timeout is None: + msg = await self._recv_queue.get() + else: + msg = await asyncio.wait_for( + self._recv_queue.get(), + timeout=timeout, + ) + except (TimeoutError, asyncio.TimeoutError): + from ..rpc.base import RecvTimeout + raise RecvTimeout(f"Receive timed out after {timeout}s") + self._last_message_time[msg.worker_id] = time.monotonic() + _check_error_and_raise(msg) + return msg + + async def try_recv(self) -> WorkerMessage | None: + """Non-blocking receive from any worker. + + If recv tasks are running (started by a previous recv() call), + checks the centralized queue. Otherwise, delegates to + _try_recv_direct() which subclasses can override to read + directly from channels. + + Raises: + PoolNotStarted: If pool hasn't been started + WorkerException: If a worker raised an exception + WorkerCrashed: If a worker died unexpectedly + WorkerTimeout: If a worker timed out + """ + if not self._running: + raise PoolNotStarted("Pool has not been started") + if self._recv_tasks: + try: + msg = self._recv_queue.get_nowait() + except asyncio.QueueEmpty: + return None + else: + msg = await self._try_recv_direct() + if msg is None: + return None + self._last_message_time[msg.worker_id] = time.monotonic() + _check_error_and_raise(msg) + return msg + + async def _monitor_loop(self) -> None: + """Background task: check worker health and silence detection.""" + _logger = logging.getLogger("netrun.pool") + while self._running: + await self._check_worker_health() + # Silence detection + if self._silence_timeout is not None: + now = time.monotonic() + for worker_id in range(self._num_workers): + last = self._last_message_time.get(worker_id) + if last is not None and (now - last) > self._silence_timeout: + _logger.warning( + "Worker %d has been silent for %.0fs (threshold: %.0fs)", + worker_id, now - last, self._silence_timeout, + ) + self._last_message_time[worker_id] = now # Reset to avoid spam + await asyncio.sleep(0.5) + + # --- Subclass contract --- + + @abstractmethod + async def _do_start(self) -> None: + """Create workers and channels. Called by start().""" + ... + + @abstractmethod + async def _do_close(self, timeout: float | None = None) -> None: + """Shut down workers and clean up. Called by close() after monitor is cancelled.""" + ... + + @abstractmethod + def _create_recv_loops(self) -> list[Coroutine]: + """Return coroutines that read from workers and put WorkerMessages on _recv_queue. + + Each coroutine should loop reading from its worker channel and forwarding + to self._recv_queue. It should catch ChannelClosed and CancelledError + and exit gracefully. + """ + ... + + @abstractmethod + async def _check_worker_health(self) -> None: + """Check if any workers have died. Put error WorkerMessages on _recv_queue for dead workers.""" + ... + + async def _try_recv_direct(self) -> WorkerMessage | None: + """Direct non-blocking receive (when recv tasks aren't running). + + Default implementation returns None. Subclasses that support direct + channel reads (ThreadPool, SingleWorkerPool) can override. + """ + return None + + @abstractmethod + async def send(self, worker_id: WorkerId, key: str, data: Any) -> None: + """Send a message to a specific worker.""" + ... + + @abstractmethod + async def broadcast(self, key: str, data: Any) -> None: + """Send a message to all workers.""" + ... + + # --- Context manager --- + + async def __aenter__(self): + await self.start() + return self + + async def __aexit__(self, *args): + await self.close() diff --git a/netrun/src/netrun/pool/multiprocess.py b/netrun/src/netrun/pool/multiprocess.py index 4316371e..04c58066 100644 --- a/netrun/src/netrun/pool/multiprocess.py +++ b/netrun/src/netrun/pool/multiprocess.py @@ -20,6 +20,7 @@ create_queue_pair, ) from ..pool.base import ( + BasePool, WorkerId, WorkerFn, WorkerMessage, @@ -422,7 +423,7 @@ def is_closed(self) -> bool: })) # %% pts/netrun/04_pool/02_multiprocess.pct.py 21 -class MultiprocessPool: +class MultiprocessPool(BasePool): """A pool of worker processes, each running multiple threads. Messages are routed to specific workers via their worker_id. @@ -438,182 +439,92 @@ def __init__( buffer_output: bool = True, output_flush_interval: float = 0.1, on_output: Callable[[int, OutputBuffer], None] | None = None, + **kwargs, ): - """Create a multiprocess pool. - - Args: - worker_fn: Function to run in each worker thread (must be importable) - num_processes: Number of subprocesses to create - threads_per_process: Number of worker threads per subprocess - redirect_output: If True, capture stdout/stderr from subprocesses - buffer_output: If True, buffer captured output for retrieval. - If False, discard captured output (silent mode). - Only applies when redirect_output=True. - output_flush_interval: Interval in seconds between automatic output buffer - flushes from subprocesses (default 0.1 = 100ms). - Only applies when redirect_output=True and buffer_output=True. - on_output: Optional callback called when output buffer is received from a - subprocess. Called with (process_idx, buffer) where buffer is - a list of (timestamp, is_stdout, text) tuples. - """ if num_processes < 1: raise ValueError("num_processes must be at least 1") if threads_per_process < 1: raise ValueError("threads_per_process must be at least 1") + super().__init__(num_workers=num_processes * threads_per_process, **kwargs) self._worker_fn = worker_fn self._num_processes = num_processes self._threads_per_process = threads_per_process - self._num_workers = num_processes * threads_per_process self._redirect_output = redirect_output self._buffer_output = buffer_output self._output_flush_interval = output_flush_interval self._on_output = on_output - self._running = False - # Will be populated on start() self._channels: list[ProcessChannel] = [] self._processes: list[mp.Process] = [] - self._recv_queue: asyncio.Queue = asyncio.Queue() - self._recv_tasks: list[asyncio.Task] = [] - self._monitor_task: asyncio.Task | None = None - self._dead_processes: set[int] = set() # Track processes we've already reported as dead - self._stdout_buffers: dict[int, OutputBuffer] = {} # process_idx -> buffer - self._flush_events: dict[int, asyncio.Event] = {} # process_idx -> event for flush response - self._shutdown_complete_events: dict[int, asyncio.Event] = {} # process_idx -> event for shutdown complete - - @property - def num_workers(self) -> int: - """Total number of workers in the pool.""" - return self._num_workers + self._dead_processes: set[int] = set() + self._stdout_buffers: dict[int, OutputBuffer] = {} + self._flush_events: dict[int, asyncio.Event] = {} + self._shutdown_complete_events: dict[int, asyncio.Event] = {} @property def num_processes(self) -> int: - """Number of subprocesses.""" return self._num_processes @property def threads_per_process(self) -> int: - """Number of threads per subprocess.""" return self._threads_per_process - @property - def is_running(self) -> bool: - """Whether the pool has been started.""" - return self._running - def _worker_id_to_process_thread(self, worker_id: WorkerId) -> tuple[int, int]: - """Convert flat worker_id to (process_idx, thread_idx).""" process_idx = worker_id // self._threads_per_process thread_idx = worker_id % self._threads_per_process return process_idx, thread_idx - async def start(self) -> None: - """Start all processes and workers.""" - if self._running: - raise PoolAlreadyStarted("Pool is already running") - + async def _do_start(self) -> None: ctx = mp.get_context("spawn") self._channels = [] self._processes = [] self._stdout_buffers = {} self._flush_events = {} self._shutdown_complete_events = {i: asyncio.Event() for i in range(self._num_processes)} + self._dead_processes = set() for process_idx in range(self._num_processes): - # Create channel pair parent_channel, child_queues = create_queue_pair(ctx) self._channels.append(parent_channel) - - # Initialize stdout buffer for this process self._stdout_buffers[process_idx] = [] - # Create and start subprocess proc = ctx.Process( target=_subprocess_main, args=( - child_queues[0], # send_q - child_queues[1], # recv_q - self._worker_fn, - self._threads_per_process, - process_idx, - self._threads_per_process, - self._redirect_output, - self._buffer_output, + child_queues[0], child_queues[1], + self._worker_fn, self._threads_per_process, + process_idx, self._threads_per_process, + self._redirect_output, self._buffer_output, self._output_flush_interval, ), ) proc.start() self._processes.append(proc) - self._running = True - self._dead_processes = set() - self._monitor_task = asyncio.create_task(self._monitor_processes()) - - async def _monitor_processes(self) -> None: - """Background task to detect dead subprocesses.""" - while self._running: - for proc_idx, proc in enumerate(self._processes): - if proc_idx not in self._dead_processes and proc.exitcode is not None: - # Process died - self._dead_processes.add(proc_idx) - exit_info = { - "exit_code": proc.exitcode, - "reason": f"Process exited with code {proc.exitcode}", - } - # Signal death for all workers in this process - for thread_idx in range(self._threads_per_process): - worker_id = proc_idx * self._threads_per_process + thread_idx - await self._recv_queue.put(WorkerMessage( - worker_id=worker_id, - key=POOL_UP_ERROR_CRASHED, - data=exit_info - )) - # Close the channel to unblock any recv_loop waiting on it - # This ensures the recv task exits rather than hanging forever - try: - await self._channels[proc_idx].close() - except Exception: - pass - # Set shutdown_complete_event so close() doesn't wait for dead processes - if proc_idx in self._shutdown_complete_events: - self._shutdown_complete_events[proc_idx].set() - await asyncio.sleep(0.5) # Check every 500ms - async def close(self, timeout: float | None = None) -> None: """Shut down all processes and clean up resources. - This method ensures that all pending output from subprocesses is received - before shutting down. It works by: - 1. Sending RPC_KEY_SHUTDOWN to each subprocess - 2. Waiting for each subprocess to send SHUTDOWN_COMPLETE (with final flush) - 3. Then closing channels and cleaning up - - Args: - timeout: Max seconds to wait for each process to finish gracefully. - If None, wait indefinitely. If timeout expires, processes - are forcefully terminated. + Overrides BasePool.close() because MultiprocessPool needs to: + 1. Start recv tasks BEFORE setting _running=False (to receive SHUTDOWN_COMPLETE) + 2. Send MP_DOWN_SHUTDOWN and wait for SHUTDOWN_COMPLETE + 3. Close channels, then cancel recv tasks, then join processes """ if not self._running: return - - # Ensure recv tasks are running BEFORE we set _running = False - # This ensures we can receive SHUTDOWN_COMPLETE messages self._start_recv_tasks() - self._running = False - # Cancel monitor task - if self._monitor_task and not self._monitor_task.done(): + # Cancel monitor + if self._monitor_task is not None and not self._monitor_task.done(): self._monitor_task.cancel() try: await self._monitor_task except asyncio.CancelledError: pass + self._monitor_task = None - # Send shutdown signals to each subprocess using MP_DOWN_SHUTDOWN (NOT RPC_KEY_SHUTDOWN) - # MP_DOWN_SHUTDOWN allows the subprocess to complete its shutdown sequence - # (including final flush and SHUTDOWN_COMPLETE) before the channel is closed + # Send shutdown to each subprocess for channel in self._channels: try: await channel.send(MP_DOWN_SHUTDOWN, None) @@ -621,24 +532,23 @@ async def close(self, timeout: float | None = None) -> None: pass # Wait for SHUTDOWN_COMPLETE from all processes - # The recv loop will set the events when it receives SHUTDOWN_COMPLETE - shutdown_timeout = timeout if timeout is not None else 30.0 # Default 30s + shutdown_timeout = timeout if timeout is not None else 30.0 try: await asyncio.wait_for( asyncio.gather(*[ self._shutdown_complete_events[i].wait() for i in range(self._num_processes) ]), - timeout=shutdown_timeout + timeout=shutdown_timeout, ) except TimeoutError: - pass # Continue with cleanup even if some processes didn't respond + pass - # Now close channels - this is safe since we've received all messages + # Close channels for channel in self._channels: await channel.close() - # Cancel recv tasks (they should already be done since they break after SHUTDOWN_COMPLETE) + # Cancel recv tasks for task in self._recv_tasks: if not task.done(): task.cancel() @@ -646,8 +556,9 @@ async def close(self, timeout: float | None = None) -> None: await task except asyncio.CancelledError: pass + self._recv_tasks.clear() - # Wait for processes to finish + # Join/terminate processes for proc in self._processes: proc.join(timeout=timeout) if proc.is_alive(): @@ -656,244 +567,136 @@ async def close(self, timeout: float | None = None) -> None: self._channels = [] self._processes = [] self._recv_queue = asyncio.Queue() - self._recv_tasks = [] - self._monitor_task = None self._dead_processes = set() self._stdout_buffers = {} self._flush_events = {} self._shutdown_complete_events = {} - async def send(self, worker_id: WorkerId, key: str, data: Any) -> None: - """Send a message to a specific worker.""" - if not self._running: - raise PoolNotStarted("Pool has not been started") - - if worker_id < 0 or worker_id >= self._num_workers: - raise ValueError(f"worker_id {worker_id} out of range [0, {self._num_workers})") - - process_idx, thread_idx = self._worker_id_to_process_thread(worker_id) - await self._channels[process_idx].send(MP_DOWN_DISPATCH, (thread_idx, key, data)) - - def _start_recv_tasks(self) -> None: - """Start background tasks that forward messages to the queue.""" - if self._recv_tasks: - return + async def _do_close(self, timeout: float | None = None) -> None: + # Not used — close() is overridden entirely + pass - async def recv_loop(process_idx: int, channel: ProcessChannel): - try: - # Keep receiving until shutdown complete or channel closed - while True: - # Use a timeout to periodically check if process is still alive - # This handles cases where the subprocess hangs during startup - try: - key, data = await channel.recv(timeout=1.0) - except RecvTimeout: - # Check if monitor has already detected and handled this dead process - # This prevents race conditions where recv_loop might exit before - # the monitor has put a CRASHED message in the queue - if process_idx in self._dead_processes: - # Monitor already handled it - safe to exit + def _create_recv_loops(self) -> list: + loops = [] + for process_idx, channel in enumerate(self._channels): + async def recv_loop(_proc_idx=process_idx, _channel=channel): + try: + while True: + try: + key, data = await _channel.recv(timeout=1.0) + except RecvTimeout: + if _proc_idx in self._dead_processes: + break + continue + + if key == MP_UP_RESPONSE: + worker_id, msg_key, msg_data = data + await self._recv_queue.put( + WorkerMessage(worker_id=worker_id, key=msg_key, data=msg_data) + ) + elif key == MP_UP_SUBPROCESS_ERROR: + for thread_idx in range(self._threads_per_process): + worker_id = _proc_idx * self._threads_per_process + thread_idx + await self._recv_queue.put(WorkerMessage( + worker_id=worker_id, key=POOL_UP_ERROR_EXCEPTION, data=data, + )) + elif key == MP_UP_STDOUT_BUFFER: + self._stdout_buffers[_proc_idx].extend(data) + if self._on_output is not None and data: + self._on_output(_proc_idx, data) + if _proc_idx in self._flush_events: + self._flush_events[_proc_idx].set() + elif key == MP_UP_SHUTDOWN_COMPLETE: + if _proc_idx in self._shutdown_complete_events: + self._shutdown_complete_events[_proc_idx].set() break - # Otherwise continue waiting (monitor will detect death if process crashed) - continue - - if key == MP_UP_RESPONSE: - worker_id, msg_key, msg_data = data - msg = WorkerMessage(worker_id=worker_id, key=msg_key, data=msg_data) - await self._recv_queue.put(msg) - elif key == MP_UP_SUBPROCESS_ERROR: - # Subprocess reported a fatal error - propagate to all workers in this process - for thread_idx in range(self._threads_per_process): - worker_id = process_idx * self._threads_per_process + thread_idx - await self._recv_queue.put(WorkerMessage( - worker_id=worker_id, - key=POOL_UP_ERROR_EXCEPTION, - data=data # dict with type, message, traceback - )) - elif key == MP_UP_STDOUT_BUFFER: - # Append received buffer to our local buffer for this process - self._stdout_buffers[process_idx].extend(data) - # Call on_output callback if provided - if self._on_output is not None and data: - self._on_output(process_idx, data) - # Signal that flush response was received - if process_idx in self._flush_events: - self._flush_events[process_idx].set() - elif key == MP_UP_SHUTDOWN_COMPLETE: - # Signal that this subprocess has finished shutdown - if process_idx in self._shutdown_complete_events: - self._shutdown_complete_events[process_idx].set() - # Exit the loop after receiving shutdown complete - break - except (ChannelClosed, asyncio.CancelledError): - pass - except Exception as e: - logging.getLogger("netrun.pool").error( - f"recv_loop for process {process_idx} crashed: {e}", exc_info=True - ) - # Notify for all workers in this process - for worker_id in range( - process_idx * self._threads_per_process, - (process_idx + 1) * self._threads_per_process, - ): + except (ChannelClosed, asyncio.CancelledError): + pass + except Exception as e: + logging.getLogger("netrun.pool").error( + f"recv_loop for process {_proc_idx} crashed: {e}", exc_info=True + ) + for worker_id in range( + _proc_idx * self._threads_per_process, + (_proc_idx + 1) * self._threads_per_process, + ): + await self._recv_queue.put(WorkerMessage( + worker_id=worker_id, key=POOL_UP_ERROR_CRASHED, + data={"reason": f"recv_loop exception: {e}"}, + )) + loops.append(recv_loop()) + return loops + + async def _check_worker_health(self) -> None: + for proc_idx, proc in enumerate(self._processes): + if proc_idx not in self._dead_processes and proc.exitcode is not None: + self._dead_processes.add(proc_idx) + exit_info = { + "exit_code": proc.exitcode, + "reason": f"Process exited with code {proc.exitcode}", + } + for thread_idx in range(self._threads_per_process): + worker_id = proc_idx * self._threads_per_process + thread_idx await self._recv_queue.put(WorkerMessage( - worker_id=worker_id, - key=POOL_UP_ERROR_CRASHED, - data={"reason": f"recv_loop exception: {e}"} + worker_id=worker_id, key=POOL_UP_ERROR_CRASHED, data=exit_info, )) + try: + await self._channels[proc_idx].close() + except Exception: + pass + if proc_idx in self._shutdown_complete_events: + self._shutdown_complete_events[proc_idx].set() - for process_idx, channel in enumerate(self._channels): - task = asyncio.create_task(recv_loop(process_idx, channel)) - self._recv_tasks.append(task) - - async def recv(self, timeout: float | None = None) -> WorkerMessage: - """Receive a message from any worker. - - Raises: - WorkerException: If the worker raised an exception - WorkerCrashed: If the worker died unexpectedly - WorkerTimeout: If the worker timed out - RecvTimeout: If this recv() call times out - """ - if not self._running: - raise PoolNotStarted("Pool has not been started") - - self._start_recv_tasks() - - try: - if timeout is None: - msg = await self._recv_queue.get() - else: - msg = await asyncio.wait_for( - self._recv_queue.get(), - timeout=timeout, - ) - except TimeoutError: - raise RecvTimeout(f"Receive timed out after {timeout}s") - - _check_error_and_raise(msg) - return msg - - async def try_recv(self) -> WorkerMessage | None: - """Non-blocking receive from any worker. - - Raises: - WorkerException: If the worker raised an exception - WorkerCrashed: If the worker died unexpectedly - WorkerTimeout: If the worker timed out - """ - if not self._running: - raise PoolNotStarted("Pool has not been started") - - # If recv tasks are running, check the queue first - if self._recv_tasks: - try: - msg = self._recv_queue.get_nowait() - _check_error_and_raise(msg) - return msg - except asyncio.QueueEmpty: - return None - - # Otherwise, read directly from channels + async def _try_recv_direct(self) -> WorkerMessage | None: + """Direct non-blocking receive (when recv tasks aren't running).""" for process_idx, channel in enumerate(self._channels): result = await channel.try_recv() if result is not None: key, data = result if key == MP_UP_RESPONSE: worker_id, msg_key, msg_data = data - msg = WorkerMessage(worker_id=worker_id, key=msg_key, data=msg_data) - _check_error_and_raise(msg) - return msg - + return WorkerMessage(worker_id=worker_id, key=msg_key, data=msg_data) return None - async def broadcast(self, key: str, data: Any) -> None: - """Send a message to all workers.""" + async def send(self, worker_id: WorkerId, key: str, data: Any) -> None: if not self._running: raise PoolNotStarted("Pool has not been started") + if worker_id < 0 or worker_id >= self._num_workers: + raise ValueError(f"worker_id {worker_id} out of range [0, {self._num_workers})") + process_idx, thread_idx = self._worker_id_to_process_thread(worker_id) + await self._channels[process_idx].send(MP_DOWN_DISPATCH, (thread_idx, key, data)) + async def broadcast(self, key: str, data: Any) -> None: + if not self._running: + raise PoolNotStarted("Pool has not been started") for channel in self._channels: await channel.send(MP_DOWN_BROADCAST, (key, data)) - async def flush_stdout(self, process_idx: int, timeout: float|None = None) -> OutputBuffer: - """Flush and retrieve stdout/stderr buffer from a specific process. - - Sends a flush request to the subprocess, waits for the response, - and returns the buffer contents. The subprocess buffer is cleared. - - Args: - process_idx: Index of the process (0 to num_processes-1) - timeout: Maximum time to wait for response in seconds - - Returns: - List of (timestamp, is_stdout, text) tuples. - is_stdout is True for stdout, False for stderr. - - Raises: - PoolNotStarted: If the pool is not running - ValueError: If process_idx is out of range - RecvTimeout: If the subprocess doesn't respond in time - """ + async def flush_stdout(self, process_idx: int, timeout: float | None = None) -> OutputBuffer: + """Flush and retrieve stdout/stderr buffer from a specific process.""" if not self._running: raise PoolNotStarted("Pool has not been started") - if process_idx < 0 or process_idx >= self._num_processes: raise ValueError(f"process_idx {process_idx} out of range [0, {self._num_processes})") - # Ensure recv tasks are running to capture the response self._start_recv_tasks() - - # Create an event to wait for the flush response event = asyncio.Event() self._flush_events[process_idx] = event - - # Send flush request await self._channels[process_idx].send(MP_DOWN_FLUSH_STDOUT, None) - - # Wait for response try: await asyncio.wait_for(event.wait(), timeout=timeout) except TimeoutError: raise RecvTimeout(f"flush_stdout timed out after {timeout}s") finally: - # Clean up event self._flush_events.pop(process_idx, None) - - # Extract buffer and clear it result = self._stdout_buffers[process_idx] self._stdout_buffers[process_idx] = [] return result - async def flush_all_stdout(self, timeout: float|None = None) -> dict[int, OutputBuffer]: - """Flush and retrieve stdout/stderr buffers from all processes. - - Args: - timeout: Maximum time to wait for each process response - - Returns: - Dictionary mapping process_idx to buffer contents. - Each buffer is a list of (timestamp, is_stdout, text) tuples. - - Raises: - PoolNotStarted: If the pool is not running - """ + async def flush_all_stdout(self, timeout: float | None = None) -> dict[int, OutputBuffer]: + """Flush and retrieve stdout/stderr buffers from all processes.""" if not self._running: raise PoolNotStarted("Pool has not been started") - - # Flush all processes concurrently - tasks = [ - self.flush_stdout(process_idx, timeout=timeout) - for process_idx in range(self._num_processes) - ] + tasks = [self.flush_stdout(i, timeout=timeout) for i in range(self._num_processes)] results = await asyncio.gather(*tasks) - return {i: result for i, result in enumerate(results)} - - async def __aenter__(self) -> "MultiprocessPool": - """Context manager entry - starts the pool.""" - await self.start() - return self - - async def __aexit__(self, exc_type, exc_val, exc_tb) -> None: - """Context manager exit - closes the pool.""" - await self.close() diff --git a/netrun/src/netrun/pool/thread.py b/netrun/src/netrun/pool/thread.py index 3275bf39..9eee3545 100644 --- a/netrun/src/netrun/pool/thread.py +++ b/netrun/src/netrun/pool/thread.py @@ -15,6 +15,7 @@ create_thread_channel_pair, ) from ..pool.base import ( + BasePool, WorkerId, WorkerFn, WorkerMessage, @@ -26,18 +27,14 @@ ) # %% pts/netrun/04_pool/01_thread.pct.py 5 -class ThreadPool: +class ThreadPool(BasePool): """A pool of worker threads. Each worker runs a user-provided function that receives messages via a sync channel and can send responses back. """ - def __init__( - self, - worker_fn: WorkerFn, - num_workers: int, - ): + def __init__(self, worker_fn: WorkerFn, num_workers: int, **kwargs): """Create a thread pool. Args: @@ -46,43 +43,21 @@ def __init__( """ if num_workers < 1: raise ValueError("num_workers must be at least 1") - + super().__init__(num_workers=num_workers, **kwargs) self._worker_fn = worker_fn - self._num_workers = num_workers - self._running = False - - # Will be populated on start() self._channels: list[ThreadChannel] = [] self._threads: list[threading.Thread] = [] - self._recv_queue: asyncio.Queue = asyncio.Queue() - self._recv_tasks: list[asyncio.Task] = [] - self._monitor_task: asyncio.Task | None = None - self._dead_workers: set[int] = set() # Track workers we've already reported as dead - - @property - def num_workers(self) -> int: - """Total number of workers in the pool.""" - return self._num_workers - - @property - def is_running(self) -> bool: - """Whether the pool has been started.""" - return self._running - - async def start(self) -> None: - """Start all workers in the pool.""" - if self._running: - raise PoolAlreadyStarted("Pool is already running") + self._dead_workers: set[int] = set() + async def _do_start(self) -> None: self._channels = [] self._threads = [] + self._dead_workers = set() for worker_id in range(self._num_workers): - # Create channel pair parent_channel, child_queues = create_thread_channel_pair() self._channels.append(parent_channel) - # Create and start worker thread thread = threading.Thread( target=self._run_worker, args=(child_queues, worker_id), @@ -92,193 +67,83 @@ async def start(self) -> None: thread.start() self._threads.append(thread) - self._running = True - self._dead_workers = set() - self._monitor_task = asyncio.create_task(self._monitor_workers()) - - async def _monitor_workers(self) -> None: - """Background task to detect dead worker threads.""" - while self._running: - for worker_id, thread in enumerate(self._threads): - if worker_id not in self._dead_workers and not thread.is_alive(): - # Thread died - send crash notification - self._dead_workers.add(worker_id) - await self._recv_queue.put(WorkerMessage( - worker_id=worker_id, - key=POOL_UP_ERROR_CRASHED, - data={"reason": "Thread exited unexpectedly"} - )) - await asyncio.sleep(0.5) # Check every 500ms - def _run_worker(self, child_queues: tuple, worker_id: WorkerId) -> None: """Run the worker function in a thread.""" send_q, recv_q = child_queues channel = SyncThreadChannel(send_q, recv_q) - try: self._worker_fn(channel, worker_id) except ChannelClosed: pass except Exception as e: - # Try to send exception object back (no serialization needed for threads) try: channel.send(POOL_UP_ERROR_EXCEPTION, e) except Exception: pass - async def close(self, timeout: float | None = None) -> None: - """Shut down all workers and clean up resources. - - Args: - timeout: Max seconds to wait for each worker to finish gracefully. - If None, wait indefinitely. - """ - if not self._running: - return - - self._running = False - - # Cancel monitor task - if self._monitor_task and not self._monitor_task.done(): - self._monitor_task.cancel() - try: - await self._monitor_task - except asyncio.CancelledError: - pass - - # Close channels first - this unblocks any recv() calls + async def _do_close(self, timeout: float | None = None) -> None: + # Close channels first — unblocks any recv() calls in workers for channel in self._channels: await channel.close() - # Now cancel recv tasks (they should exit quickly since channels are closed) - for task in self._recv_tasks: - if not task.done(): - task.cancel() - try: - await task - except asyncio.CancelledError: - pass - # Wait for threads to finish for thread in self._threads: thread.join(timeout=timeout) self._channels = [] self._threads = [] - self._recv_queue = asyncio.Queue() - self._recv_tasks = [] - self._monitor_task = None self._dead_workers = set() - async def send(self, worker_id: WorkerId, key: str, data: Any) -> None: - """Send a message to a specific worker.""" - if not self._running: - raise PoolNotStarted("Pool has not been started") - - if worker_id < 0 or worker_id >= self._num_workers: - raise ValueError(f"worker_id {worker_id} out of range [0, {self._num_workers})") - - await self._channels[worker_id].send(key, data) - - def _start_recv_tasks(self) -> None: - """Start background tasks that forward messages to the queue.""" - if self._recv_tasks: - return + def _create_recv_loops(self) -> list: + loops = [] + for worker_id, channel in enumerate(self._channels): + async def recv_loop(_worker_id=worker_id, _channel=channel): + try: + while self._running: + key, data = await _channel.recv() + await self._recv_queue.put( + WorkerMessage(worker_id=_worker_id, key=key, data=data) + ) + except (ChannelClosed, asyncio.CancelledError): + pass + except Exception as e: + logging.getLogger("netrun.pool").error( + f"recv_loop for worker {_worker_id} crashed: {e}", exc_info=True + ) + await self._recv_queue.put(WorkerMessage( + worker_id=_worker_id, key=POOL_UP_ERROR_CRASHED, + data={"reason": f"recv_loop exception: {e}"}, + )) + loops.append(recv_loop()) + return loops - async def recv_loop(worker_id: WorkerId, channel: ThreadChannel): - try: - while self._running: - key, data = await channel.recv() - msg = WorkerMessage(worker_id=worker_id, key=key, data=data) - await self._recv_queue.put(msg) - except (ChannelClosed, asyncio.CancelledError): - pass - except Exception as e: - logging.getLogger("netrun.pool").error( - f"recv_loop for worker {worker_id} crashed: {e}", exc_info=True - ) + async def _check_worker_health(self) -> None: + for worker_id, thread in enumerate(self._threads): + if worker_id not in self._dead_workers and not thread.is_alive(): + self._dead_workers.add(worker_id) await self._recv_queue.put(WorkerMessage( - worker_id=worker_id, - key=POOL_UP_ERROR_CRASHED, - data={"reason": f"recv_loop exception: {e}"} + worker_id=worker_id, key=POOL_UP_ERROR_CRASHED, + data={"reason": "Thread exited unexpectedly"}, )) - for worker_id, channel in enumerate(self._channels): - task = asyncio.create_task(recv_loop(worker_id, channel)) - self._recv_tasks.append(task) - - async def recv(self, timeout: float | None = None) -> WorkerMessage: - """Receive a message from any worker. - - Raises: - WorkerException: If the worker raised an exception - WorkerCrashed: If the worker died unexpectedly - WorkerTimeout: If the worker timed out - RecvTimeout: If this recv() call times out - """ - if not self._running: - raise PoolNotStarted("Pool has not been started") - - self._start_recv_tasks() - - try: - if timeout is None: - msg = await self._recv_queue.get() - else: - msg = await asyncio.wait_for( - self._recv_queue.get(), - timeout=timeout, - ) - except TimeoutError: - raise RecvTimeout(f"Receive timed out after {timeout}s") - - _check_error_and_raise(msg) - return msg - - async def try_recv(self) -> WorkerMessage | None: - """Non-blocking receive from any worker. - - Raises: - WorkerException: If the worker raised an exception - WorkerCrashed: If the worker died unexpectedly - WorkerTimeout: If the worker timed out - """ - if not self._running: - raise PoolNotStarted("Pool has not been started") - - # If recv tasks are running, check the queue first - if self._recv_tasks: - try: - msg = self._recv_queue.get_nowait() - _check_error_and_raise(msg) - return msg - except asyncio.QueueEmpty: - return None - - # Otherwise, read directly from channels + async def _try_recv_direct(self) -> WorkerMessage | None: + """Read directly from channels (when recv tasks aren't running).""" for worker_id, channel in enumerate(self._channels): result = await channel.try_recv() if result is not None: key, data = result - msg = WorkerMessage(worker_id=worker_id, key=key, data=data) - _check_error_and_raise(msg) - return msg - + return WorkerMessage(worker_id=worker_id, key=key, data=data) return None - async def broadcast(self, key: str, data: Any) -> None: - """Send a message to all workers.""" + async def send(self, worker_id: WorkerId, key: str, data: Any) -> None: if not self._running: raise PoolNotStarted("Pool has not been started") + if worker_id < 0 or worker_id >= self._num_workers: + raise ValueError(f"worker_id {worker_id} out of range [0, {self._num_workers})") + await self._channels[worker_id].send(key, data) - for worker_id in range(self._num_workers): - await self._channels[worker_id].send(key, data) - - async def __aenter__(self) -> "ThreadPool": - """Context manager entry - starts the pool.""" - await self.start() - return self - - async def __aexit__(self, exc_type, exc_val, exc_tb) -> None: - """Context manager exit - closes the pool.""" - await self.close() + async def broadcast(self, key: str, data: Any) -> None: + if not self._running: + raise PoolNotStarted("Pool has not been started") + for channel in self._channels: + await channel.send(key, data) From 77c42a623cdb8155e5ad04a34cb43b2955a5f8ce Mon Sep 17 00:00:00 2001 From: Lukas Kikuchi Date: Sat, 25 Apr 2026 20:29:42 +0100 Subject: [PATCH 05/15] refactor(netrun): remove unused config fields, consolidate epoch state Phase 4 of the major refactor. Cleans up config/runtime drift and consolidates scattered epoch state into a single data structure. Config fields removed (unused or partially wired): - NodeExecutionConfig: defer_net_actions, capture_prints, print_flush_interval, print_buffer_max_size - NetConfig: dead_letter_callback, dead_letter_path - PoolConfig: capture_prints, print_flush_interval These fields were defined and resolved but never read at runtime, creating misleading configuration knobs. Epoch state consolidated: - Moved structured_logs from _epoch_log_buffers dict onto _EpochState - Moved net_actions from _epoch_net_actions dict onto _EpochState - Moved retry_state (ctx.state) from _epoch_states dict onto _EpochState - Removed three separate dicts, all state now lives on the epoch record - Easier to reason about epoch lifecycle, less error-prone cleanup Co-Authored-By: Claude Opus 4.6 (1M context) --- .../netrun/06_net/00_config/01_nodes.pct.py | 8 ---- .../06_net/00_config/03_net_config.pct.py | 27 -------------- .../netrun/06_net/01_net/00_context.pct.py | 16 +++----- netrun/pts/netrun/06_net/01_net/02_net.pct.py | 37 ++++++------------- netrun/pts/tests/06_net/test_config.pct.py | 20 +++++----- netrun/pts/tests/06_net/test_net.pct.py | 10 +---- netrun/src/netrun/net/_net/_context.py | 14 +++---- netrun/src/netrun/net/_net/_net.py | 37 ++++++------------- netrun/src/netrun/net/config/_net_config.py | 27 -------------- netrun/src/netrun/net/config/_nodes.py | 8 ---- netrun/src/tests/net/test_config.py | 20 +++++----- netrun/src/tests/net/test_net.py | 10 +---- 12 files changed, 55 insertions(+), 179 deletions(-) diff --git a/netrun/pts/netrun/06_net/00_config/01_nodes.pct.py b/netrun/pts/netrun/06_net/00_config/01_nodes.pct.py index 6872d5cb..43238256 100644 --- a/netrun/pts/netrun/06_net/00_config/01_nodes.pct.py +++ b/netrun/pts/netrun/06_net/00_config/01_nodes.pct.py @@ -271,18 +271,10 @@ class NodeExecutionConfig(VarResolvableModel): resources: dict[str, int] | None = Field(default=None, description="Resource requirements as {resource_name: slots_needed}. Resources must be defined in NetConfig.resources with their capacity. Node epoch starts only when all required resource slots are available.") - defer_net_actions: bool | VarRef | None = Field(default=None, description="Defer net action notifications until epoch completes successfully. Required if retries enabled.") - retries: int | VarRef | None = Field(default=None, description="Number of retry attempts on failure. None inherits from NetConfig.") retry_wait: float | VarRef | None = Field(default=None, description="Wait time in seconds between retries. None inherits from NetConfig.") timeout: float | VarRef | None = Field(default=None, description="Epoch execution timeout in seconds. None inherits from NetConfig.") - capture_prints: bool | VarRef = Field(default=True, description="Capture print statements in the node.") - - print_flush_interval: float | VarRef = Field(default=0.1, description="How often to flush the print buffer in seconds.") - - print_buffer_max_size: int | VarRef | None = Field(default=None, description="Max print buffer size before forced flush. None = unlimited.") - print_echo_stdout: bool | VarRef | None = Field(default=None, description="Also print to actual stdout when ctx.print() is called. None inherits from NetConfig.") pool_allocation_method: RunAllocationMethod | VarRef | None = Field(default=None, description="Worker selection method when node has multiple pools. None inherits Net default.") diff --git a/netrun/pts/netrun/06_net/00_config/03_net_config.pct.py b/netrun/pts/netrun/06_net/00_config/03_net_config.pct.py index 24fc64ff..41103573 100644 --- a/netrun/pts/netrun/06_net/00_config/03_net_config.pct.py +++ b/netrun/pts/netrun/06_net/00_config/03_net_config.pct.py @@ -112,8 +112,6 @@ class RemotePoolConfig(VarResolvableModel): class PoolConfig(VarResolvableModel): """Configuration for a pool of workers.""" - print_flush_interval: float | VarRef = 0.1 - capture_prints: bool | VarRef = True spec: PoolSpecConfig = Field(default_factory=MainPoolConfig) # %% [markdown] @@ -327,8 +325,6 @@ def from_file( node_vars: dict[str, NodeVariable] | None = Field(default=None, description="Global default node variables, accessible via ctx.vars.") dead_letter_queue: bool | VarRef = Field(default=True, description="Enable dead letter queue for undeliverable packets.") - dead_letter_path: Annotated[str | VarRef | None, ProjectRootPath()] = Field(default=None, description="File path for dead letter queue storage.") - dead_letter_callback: Callable | str | VarRef | None = Field(default=None, description="Callback function or import path for dead letter handling.") # Output queue configuration output_queues: dict[str, OutputQueueConfig] | None = Field(default=None, description="Output queue configurations. None auto-generates queues for unconnected output ports.") @@ -359,24 +355,6 @@ def from_file( default_controls: list[str] | VarRef = Field(default_factory=list, description="Default control types for all nodes. Nodes inherit this unless they set their own controls list.") - @field_serializer("dead_letter_callback", when_used='json') - def serialize_dead_letter_callback(self, callback: Callable | str | VarRef | None) -> str | dict | None: - """Serialize dead_letter_callback to import path for JSON. - - Note: Only called during JSON serialization (model_dump_json). - EnvVar instances are serialized to their dict form. - - Raises: - ValueError: If callback is defined in __main__, is a lambda, or is a closure. - """ - if callback is None: - return None - if isinstance(callback, VarRef): - return callback.model_dump(by_alias=True) - if isinstance(callback, str): - return callback - return _get_callable_import_path(callback) - @field_validator("default_signals") @classmethod def validate_default_signals(cls, v): @@ -417,7 +395,6 @@ def resolve(self, base_path: Path | None = None) -> "NetConfig": - All subgraphs in the graph (flattening to regular nodes) - All node factories in the graph - All string import paths to callables - - dead_letter_callback if it's a string - pools if None (generates default main pool) - Per-node $var resolution with merged vars (net + node override) in GraphConfig.resolve @@ -490,10 +467,6 @@ def resolve(self, base_path: Path | None = None) -> "NetConfig": if resolved.output_queues is None: updates["output_queues"] = _generate_default_output_queues(resolved_graph) - # Resolve dead_letter_callback - if isinstance(resolved.dead_letter_callback, str): - updates["dead_letter_callback"] = _import_from_path(resolved.dead_letter_callback, project_root=project_root) - if updates: result = resolved.model_copy(update=updates) # Pydantic v2 model_copy() does not copy PrivateAttr - preserve it diff --git a/netrun/pts/netrun/06_net/01_net/00_context.pct.py b/netrun/pts/netrun/06_net/01_net/00_context.pct.py index 94eecb7e..b0e09902 100644 --- a/netrun/pts/netrun/06_net/01_net/00_context.pct.py +++ b/netrun/pts/netrun/06_net/01_net/00_context.pct.py @@ -955,6 +955,11 @@ class _EpochState: was_cache_hit: bool = False was_file_storage_hit: bool = False + # Consolidated from previously-separate dicts: + structured_logs: list = field(default_factory=list) # was _epoch_log_buffers[epoch_id] + net_actions: list = field(default_factory=list) # was _epoch_net_actions[epoch_id] + retry_state: dict = field(default_factory=dict) # was _epoch_states[epoch_id] (ctx.state) + @classmethod def from_epoch(cls, epoch) -> "_EpochState": """Create an _EpochState from a netrun_sim.Epoch object.""" @@ -1082,7 +1087,7 @@ def to_log( # %% [markdown] # ## Deferred Action Queue # -# For `defer_net_actions=True`, packet operations are queued and only committed on success. +# Packet operations are queued during node execution and committed on success or discarded on failure. # %% #|export @@ -1147,9 +1152,6 @@ class NetFuncPreprocessorNodeConfig: """Input port configs (serialized as dicts for pickling).""" # Copy of NodeExecutionConfig fields needed for context creation - capture_prints: bool = True - print_flush_interval: float = 0.1 - print_buffer_max_size: int | None = None print_echo_stdout: bool = False retries: int = 0 retry_wait: float = 0.0 @@ -1189,9 +1191,6 @@ def from_node_config( factory_args=factory_args, in_ports={name: port.model_dump() for name, port in in_ports.items()}, out_ports={name: port.model_dump() for name, port in out_ports.items()}, - capture_prints=exec_config.capture_prints, - print_flush_interval=exec_config.print_flush_interval, - print_buffer_max_size=exec_config.print_buffer_max_size, print_echo_stdout=ef.get("print_echo_stdout", False), retries=ef.get("retries", 0), retry_wait=ef.get("retry_wait", 0.0), @@ -1290,9 +1289,6 @@ def _setup_context( exec_config = None if config: exec_config = NodeExecutionConfig( - capture_prints=config.capture_prints, - print_flush_interval=config.print_flush_interval, - print_buffer_max_size=config.print_buffer_max_size, print_echo_stdout=config.print_echo_stdout, retries=config.retries, retry_wait=config.retry_wait, diff --git a/netrun/pts/netrun/06_net/01_net/02_net.pct.py b/netrun/pts/netrun/06_net/01_net/02_net.pct.py index 7f993795..e8335bc4 100644 --- a/netrun/pts/netrun/06_net/01_net/02_net.pct.py +++ b/netrun/pts/netrun/06_net/01_net/02_net.pct.py @@ -434,21 +434,16 @@ def __init__(self, config: NetConfig, run_init_nodes: bool = True): # Running epoch tracking self._running_epochs: set[str] = set() - # Cross-retry state dicts (epoch_id -> mutable dict accessible via ctx.state) - self._epoch_states: dict[str, dict[str, Any]] = {} - # Scheduling constraint tracking self._node_has_completed: set[str] = set() # Nodes that have completed at least one epoch self._resource_usage: dict[str, int] = {} # Current resource slot usage per resource name self._resource_capacities: dict[str, int] = dict(self._config_resolved.resources) if self._config_resolved.resources else {} - # Epoch state (persists after epoch finishes in netsim) + # Epoch state (persists after epoch finishes in netsim). + # Consolidated: structured_logs, net_actions, and retry_state (ctx.state) + # are all stored on each _EpochState object. self._epochs: dict[str, _EpochState] = {} # epoch_id -> _EpochState - # Per-epoch buffers for structured logs and net actions (populated during execution) - self._epoch_log_buffers: dict[str, list[NodeLogEntry]] = {} - self._epoch_net_actions: dict[str, list[NetActionLog]] = {} - # Per run_step buffers (cleared at start of each run_step) self._step_net_actions: list[NetActionLog] = [] self._step_epoch_logs: list[EpochLog] = [] @@ -1582,8 +1577,8 @@ def _do_action(self, action, *, epoch_id: str | None = None, detail: dict | None self._step_net_actions.append(net_action) # Add to epoch buffer if attributed - if epoch_id and epoch_id in self._epoch_net_actions: - self._epoch_net_actions[epoch_id].append(net_action) + if epoch_id and epoch_id in self._epochs: + self._epochs[epoch_id].net_actions.append(net_action) # Retain if configured if self._config_resolved.retain_net_action_logs: @@ -2030,7 +2025,7 @@ async def _finish_epoch_lifecycle(self, epoch_id: str, node_name: str, *, retry_ self._do_action(netrun_sim.NetAction.finish_epoch(epoch_id), epoch_id=epoch_id, detail={"node_name": node_name}) self._epochs[epoch_id].ended_at = get_timestamp_utc() self._epochs[epoch_id].state = netrun_sim.EpochState.Finished - self._epoch_states.pop(epoch_id, None) # Clear cross-retry state on success + self._epochs[epoch_id].retry_state.clear() # Clear cross-retry state on success self._node_has_completed.add(node_name) # Mark node as having completed (for depends_on) await self._fire_epoch_end(node_name, epoch_id, retry_count=retry_count) @@ -2210,8 +2205,6 @@ async def _execute_epoch(self, epoch_id: str) -> NodeExecutionResult | None: epoch = self._netsim.get_epoch(epoch_id) node_name = epoch.node_name self._epochs[epoch_id] = _EpochState.from_epoch(epoch) - self._epoch_log_buffers[epoch_id] = [] - self._epoch_net_actions[epoch_id] = [] config = self._get_node_execution_config(node_name) # Check if this is a control epoch @@ -2401,10 +2394,6 @@ async def _execute_epoch_with_retry( # Get func key for this node func_key = self._get_func_key(node_name) - # Initialize cross-retry state on first attempt - if retry_count == 0: - self._epoch_states[epoch_id] = {} - # Dispatch to worker (with optional timeout) try: coro = self._execution_manager.run_allocate( @@ -2417,7 +2406,7 @@ async def _execute_epoch_with_retry( "retry_count": retry_count, "retry_timestamps": retry_timestamps, "retry_exceptions": retry_exceptions, - "state": self._epoch_states.get(epoch_id, {}), + "state": self._epochs[epoch_id].retry_state, }, ) effective_timeout = resolve_effective_exec_field("timeout", config, self._config_resolved) @@ -2455,7 +2444,7 @@ async def _execute_epoch_with_retry( # Handle structured log buffer if execution_result.structured_log_buffer: - self._epoch_log_buffers.setdefault(epoch_id, []).extend( + self._epochs[epoch_id].structured_logs.extend( execution_result.structured_log_buffer ) @@ -3042,7 +3031,7 @@ async def _handle_epoch_failure( record.was_cancelled = True record.ended_at = get_timestamp_utc() record.destroyed_packets = list(response.destroyed_packets) - self._epoch_states.pop(epoch_id, None) # Clear cross-retry state on permanent failure + self._epochs[epoch_id].retry_state.clear() # Clear cross-retry state on permanent failure await self._fire_epoch_end(node_name, epoch_id, error=error, retry_count=retry_count) # Store in dead letter queue @@ -4180,8 +4169,8 @@ async def _fire_epoch_end(self, node_name: str, epoch_id: str, *, error: Excepti # Assemble EpochLog epoch_log = record.to_log( - node_log_entries=self._epoch_log_buffers.get(epoch_id, []), - net_actions=self._epoch_net_actions.get(epoch_id, []), + node_log_entries=record.structured_logs, + net_actions=record.net_actions, factory=factory, retry_count=retry_count, error=error, @@ -4200,10 +4189,6 @@ async def _fire_epoch_end(self, node_name: str, epoch_id: str, *, error: Excepti import builtins builtins.print(f"[{ts}] [{node_name}] {_format_epoch_log(epoch_log)}", flush=True) - # Clean up per-epoch buffers - self._epoch_log_buffers.pop(epoch_id, None) - self._epoch_net_actions.pop(epoch_id, None) - # Clean up epoch state when not retaining logs if not self._config_resolved.retain_epoch_logs: self._epochs.pop(epoch_id, None) diff --git a/netrun/pts/tests/06_net/test_config.pct.py b/netrun/pts/tests/06_net/test_config.pct.py index 8c349ce5..698951bc 100644 --- a/netrun/pts/tests/06_net/test_config.pct.py +++ b/netrun/pts/tests/06_net/test_config.pct.py @@ -3024,14 +3024,13 @@ def test_resolve_env_vars_missing_no_default(monkeypatch): # %% #|export def test_resolve_env_vars_nested_models(monkeypatch): - """PoolConfig containing ThreadPoolConfig with EnvVar.""" - monkeypatch.setenv("FLUSH_INTERVAL", "0.5") - cfg = PoolConfig( - print_flush_interval=EnvVar(env="FLUSH_INTERVAL"), - spec=ThreadPoolConfig(num_workers=2), + """NodeExecutionConfig with EnvVar in nested field.""" + monkeypatch.setenv("RETRY_WAIT", "2.5") + cfg = NodeExecutionConfig( + retry_wait=EnvVar(env="RETRY_WAIT"), ) resolved = cfg.resolve_env_vars() - assert resolved.print_flush_interval == 0.5 + assert resolved.retry_wait == 2.5 # %% test_resolve_env_vars_nested_models() @@ -3104,15 +3103,14 @@ def test_resolve_env_vars_no_envvars_returns_self(): # %% #|export def test_netconfig_resolve_env_vars_before_imports(monkeypatch): - """Full NetConfig.resolve() with env var in dead_letter_callback.""" - monkeypatch.setenv("DL_CALLBACK", "json.loads") + """Full NetConfig.resolve() with env var in print_echo_stdout.""" + monkeypatch.setenv("ECHO_STDOUT", "false") nc = NetConfig( graph=GraphConfig(nodes=[]), - dead_letter_callback=EnvVar(env="DL_CALLBACK"), + print_echo_stdout=EnvVar(env="ECHO_STDOUT"), ) resolved = nc.resolve() - import json as json_mod - assert resolved.dead_letter_callback is json_mod.loads + assert resolved.print_echo_stdout is False # %% test_netconfig_resolve_env_vars_before_imports() diff --git a/netrun/pts/tests/06_net/test_net.pct.py b/netrun/pts/tests/06_net/test_net.pct.py index 59c8db30..b7af0c89 100644 --- a/netrun/pts/tests/06_net/test_net.pct.py +++ b/netrun/pts/tests/06_net/test_net.pct.py @@ -1637,17 +1637,13 @@ def dummy(ctx, packets): # %% #|export def test_node_execution_config_new_fields(): - """Test NodeExecutionConfig has new print config fields.""" + """Test NodeExecutionConfig has config fields.""" config = NodeExecutionConfig( node_name="TestNode", - print_flush_interval=0.2, - print_buffer_max_size=100, print_echo_stdout=True, pool_allocation_method=RunAllocationMethod.LEAST_BUSY, ) - assert config.print_flush_interval == 0.2 - assert config.print_buffer_max_size == 100 assert config.print_echo_stdout is True assert config.pool_allocation_method == RunAllocationMethod.LEAST_BUSY @@ -1657,11 +1653,9 @@ def test_node_execution_config_new_fields(): # %% #|export def test_node_execution_config_defaults(): - """Test NodeExecutionConfig has correct defaults for new fields.""" + """Test NodeExecutionConfig has correct defaults.""" config = NodeExecutionConfig() - assert config.print_flush_interval == 0.1 - assert config.print_buffer_max_size is None assert config.print_echo_stdout is None assert config.pool_allocation_method is None diff --git a/netrun/src/netrun/net/_net/_context.py b/netrun/src/netrun/net/_net/_context.py index 1257ff22..9338860c 100644 --- a/netrun/src/netrun/net/_net/_context.py +++ b/netrun/src/netrun/net/_net/_context.py @@ -887,6 +887,11 @@ class _EpochState: was_cache_hit: bool = False was_file_storage_hit: bool = False + # Consolidated from previously-separate dicts: + structured_logs: list = field(default_factory=list) # was _epoch_log_buffers[epoch_id] + net_actions: list = field(default_factory=list) # was _epoch_net_actions[epoch_id] + retry_state: dict = field(default_factory=dict) # was _epoch_states[epoch_id] (ctx.state) + @classmethod def from_epoch(cls, epoch) -> "_EpochState": """Create an _EpochState from a netrun_sim.Epoch object.""" @@ -1066,9 +1071,6 @@ class NetFuncPreprocessorNodeConfig: """Input port configs (serialized as dicts for pickling).""" # Copy of NodeExecutionConfig fields needed for context creation - capture_prints: bool = True - print_flush_interval: float = 0.1 - print_buffer_max_size: int | None = None print_echo_stdout: bool = False retries: int = 0 retry_wait: float = 0.0 @@ -1108,9 +1110,6 @@ def from_node_config( factory_args=factory_args, in_ports={name: port.model_dump() for name, port in in_ports.items()}, out_ports={name: port.model_dump() for name, port in out_ports.items()}, - capture_prints=exec_config.capture_prints, - print_flush_interval=exec_config.print_flush_interval, - print_buffer_max_size=exec_config.print_buffer_max_size, print_echo_stdout=ef.get("print_echo_stdout", False), retries=ef.get("retries", 0), retry_wait=ef.get("retry_wait", 0.0), @@ -1209,9 +1208,6 @@ def _setup_context( exec_config = None if config: exec_config = NodeExecutionConfig( - capture_prints=config.capture_prints, - print_flush_interval=config.print_flush_interval, - print_buffer_max_size=config.print_buffer_max_size, print_echo_stdout=config.print_echo_stdout, retries=config.retries, retry_wait=config.retry_wait, diff --git a/netrun/src/netrun/net/_net/_net.py b/netrun/src/netrun/net/_net/_net.py index 62c26eb7..1b5ef7f0 100644 --- a/netrun/src/netrun/net/_net/_net.py +++ b/netrun/src/netrun/net/_net/_net.py @@ -408,21 +408,16 @@ def __init__(self, config: NetConfig, run_init_nodes: bool = True): # Running epoch tracking self._running_epochs: set[str] = set() - # Cross-retry state dicts (epoch_id -> mutable dict accessible via ctx.state) - self._epoch_states: dict[str, dict[str, Any]] = {} - # Scheduling constraint tracking self._node_has_completed: set[str] = set() # Nodes that have completed at least one epoch self._resource_usage: dict[str, int] = {} # Current resource slot usage per resource name self._resource_capacities: dict[str, int] = dict(self._config_resolved.resources) if self._config_resolved.resources else {} - # Epoch state (persists after epoch finishes in netsim) + # Epoch state (persists after epoch finishes in netsim). + # Consolidated: structured_logs, net_actions, and retry_state (ctx.state) + # are all stored on each _EpochState object. self._epochs: dict[str, _EpochState] = {} # epoch_id -> _EpochState - # Per-epoch buffers for structured logs and net actions (populated during execution) - self._epoch_log_buffers: dict[str, list[NodeLogEntry]] = {} - self._epoch_net_actions: dict[str, list[NetActionLog]] = {} - # Per run_step buffers (cleared at start of each run_step) self._step_net_actions: list[NetActionLog] = [] self._step_epoch_logs: list[EpochLog] = [] @@ -1556,8 +1551,8 @@ def _do_action(self, action, *, epoch_id: str | None = None, detail: dict | None self._step_net_actions.append(net_action) # Add to epoch buffer if attributed - if epoch_id and epoch_id in self._epoch_net_actions: - self._epoch_net_actions[epoch_id].append(net_action) + if epoch_id and epoch_id in self._epochs: + self._epochs[epoch_id].net_actions.append(net_action) # Retain if configured if self._config_resolved.retain_net_action_logs: @@ -2004,7 +1999,7 @@ async def _finish_epoch_lifecycle(self, epoch_id: str, node_name: str, *, retry_ self._do_action(netrun_sim.NetAction.finish_epoch(epoch_id), epoch_id=epoch_id, detail={"node_name": node_name}) self._epochs[epoch_id].ended_at = get_timestamp_utc() self._epochs[epoch_id].state = netrun_sim.EpochState.Finished - self._epoch_states.pop(epoch_id, None) # Clear cross-retry state on success + self._epochs[epoch_id].retry_state.clear() # Clear cross-retry state on success self._node_has_completed.add(node_name) # Mark node as having completed (for depends_on) await self._fire_epoch_end(node_name, epoch_id, retry_count=retry_count) @@ -2184,8 +2179,6 @@ async def _execute_epoch(self, epoch_id: str) -> NodeExecutionResult | None: epoch = self._netsim.get_epoch(epoch_id) node_name = epoch.node_name self._epochs[epoch_id] = _EpochState.from_epoch(epoch) - self._epoch_log_buffers[epoch_id] = [] - self._epoch_net_actions[epoch_id] = [] config = self._get_node_execution_config(node_name) # Check if this is a control epoch @@ -2375,10 +2368,6 @@ async def _execute_epoch_with_retry( # Get func key for this node func_key = self._get_func_key(node_name) - # Initialize cross-retry state on first attempt - if retry_count == 0: - self._epoch_states[epoch_id] = {} - # Dispatch to worker (with optional timeout) try: coro = self._execution_manager.run_allocate( @@ -2391,7 +2380,7 @@ async def _execute_epoch_with_retry( "retry_count": retry_count, "retry_timestamps": retry_timestamps, "retry_exceptions": retry_exceptions, - "state": self._epoch_states.get(epoch_id, {}), + "state": self._epochs[epoch_id].retry_state, }, ) effective_timeout = resolve_effective_exec_field("timeout", config, self._config_resolved) @@ -2429,7 +2418,7 @@ async def _execute_epoch_with_retry( # Handle structured log buffer if execution_result.structured_log_buffer: - self._epoch_log_buffers.setdefault(epoch_id, []).extend( + self._epochs[epoch_id].structured_logs.extend( execution_result.structured_log_buffer ) @@ -3016,7 +3005,7 @@ async def _handle_epoch_failure( record.was_cancelled = True record.ended_at = get_timestamp_utc() record.destroyed_packets = list(response.destroyed_packets) - self._epoch_states.pop(epoch_id, None) # Clear cross-retry state on permanent failure + self._epochs[epoch_id].retry_state.clear() # Clear cross-retry state on permanent failure await self._fire_epoch_end(node_name, epoch_id, error=error, retry_count=retry_count) # Store in dead letter queue @@ -4154,8 +4143,8 @@ async def _fire_epoch_end(self, node_name: str, epoch_id: str, *, error: Excepti # Assemble EpochLog epoch_log = record.to_log( - node_log_entries=self._epoch_log_buffers.get(epoch_id, []), - net_actions=self._epoch_net_actions.get(epoch_id, []), + node_log_entries=record.structured_logs, + net_actions=record.net_actions, factory=factory, retry_count=retry_count, error=error, @@ -4174,10 +4163,6 @@ async def _fire_epoch_end(self, node_name: str, epoch_id: str, *, error: Excepti import builtins builtins.print(f"[{ts}] [{node_name}] {_format_epoch_log(epoch_log)}", flush=True) - # Clean up per-epoch buffers - self._epoch_log_buffers.pop(epoch_id, None) - self._epoch_net_actions.pop(epoch_id, None) - # Clean up epoch state when not retaining logs if not self._config_resolved.retain_epoch_logs: self._epochs.pop(epoch_id, None) diff --git a/netrun/src/netrun/net/config/_net_config.py b/netrun/src/netrun/net/config/_net_config.py index f0ad2a4a..99a1641d 100644 --- a/netrun/src/netrun/net/config/_net_config.py +++ b/netrun/src/netrun/net/config/_net_config.py @@ -85,8 +85,6 @@ class RemotePoolConfig(VarResolvableModel): class PoolConfig(VarResolvableModel): """Configuration for a pool of workers.""" - print_flush_interval: float | VarRef = 0.1 - capture_prints: bool | VarRef = True spec: PoolSpecConfig = Field(default_factory=MainPoolConfig) # %% pts/netrun/06_net/00_config/03_net_config.pct.py 9 @@ -292,8 +290,6 @@ def from_file( node_vars: dict[str, NodeVariable] | None = Field(default=None, description="Global default node variables, accessible via ctx.vars.") dead_letter_queue: bool | VarRef = Field(default=True, description="Enable dead letter queue for undeliverable packets.") - dead_letter_path: Annotated[str | VarRef | None, ProjectRootPath()] = Field(default=None, description="File path for dead letter queue storage.") - dead_letter_callback: Callable | str | VarRef | None = Field(default=None, description="Callback function or import path for dead letter handling.") # Output queue configuration output_queues: dict[str, OutputQueueConfig] | None = Field(default=None, description="Output queue configurations. None auto-generates queues for unconnected output ports.") @@ -324,24 +320,6 @@ def from_file( default_controls: list[str] | VarRef = Field(default_factory=list, description="Default control types for all nodes. Nodes inherit this unless they set their own controls list.") - @field_serializer("dead_letter_callback", when_used='json') - def serialize_dead_letter_callback(self, callback: Callable | str | VarRef | None) -> str | dict | None: - """Serialize dead_letter_callback to import path for JSON. - - Note: Only called during JSON serialization (model_dump_json). - EnvVar instances are serialized to their dict form. - - Raises: - ValueError: If callback is defined in __main__, is a lambda, or is a closure. - """ - if callback is None: - return None - if isinstance(callback, VarRef): - return callback.model_dump(by_alias=True) - if isinstance(callback, str): - return callback - return _get_callable_import_path(callback) - @field_validator("default_signals") @classmethod def validate_default_signals(cls, v): @@ -382,7 +360,6 @@ def resolve(self, base_path: Path | None = None) -> "NetConfig": - All subgraphs in the graph (flattening to regular nodes) - All node factories in the graph - All string import paths to callables - - dead_letter_callback if it's a string - pools if None (generates default main pool) - Per-node $var resolution with merged vars (net + node override) in GraphConfig.resolve @@ -455,10 +432,6 @@ def resolve(self, base_path: Path | None = None) -> "NetConfig": if resolved.output_queues is None: updates["output_queues"] = _generate_default_output_queues(resolved_graph) - # Resolve dead_letter_callback - if isinstance(resolved.dead_letter_callback, str): - updates["dead_letter_callback"] = _import_from_path(resolved.dead_letter_callback, project_root=project_root) - if updates: result = resolved.model_copy(update=updates) # Pydantic v2 model_copy() does not copy PrivateAttr - preserve it diff --git a/netrun/src/netrun/net/config/_nodes.py b/netrun/src/netrun/net/config/_nodes.py index d79f4da9..1e008bc7 100644 --- a/netrun/src/netrun/net/config/_nodes.py +++ b/netrun/src/netrun/net/config/_nodes.py @@ -236,18 +236,10 @@ class NodeExecutionConfig(VarResolvableModel): resources: dict[str, int] | None = Field(default=None, description="Resource requirements as {resource_name: slots_needed}. Resources must be defined in NetConfig.resources with their capacity. Node epoch starts only when all required resource slots are available.") - defer_net_actions: bool | VarRef | None = Field(default=None, description="Defer net action notifications until epoch completes successfully. Required if retries enabled.") - retries: int | VarRef | None = Field(default=None, description="Number of retry attempts on failure. None inherits from NetConfig.") retry_wait: float | VarRef | None = Field(default=None, description="Wait time in seconds between retries. None inherits from NetConfig.") timeout: float | VarRef | None = Field(default=None, description="Epoch execution timeout in seconds. None inherits from NetConfig.") - capture_prints: bool | VarRef = Field(default=True, description="Capture print statements in the node.") - - print_flush_interval: float | VarRef = Field(default=0.1, description="How often to flush the print buffer in seconds.") - - print_buffer_max_size: int | VarRef | None = Field(default=None, description="Max print buffer size before forced flush. None = unlimited.") - print_echo_stdout: bool | VarRef | None = Field(default=None, description="Also print to actual stdout when ctx.print() is called. None inherits from NetConfig.") pool_allocation_method: RunAllocationMethod | VarRef | None = Field(default=None, description="Worker selection method when node has multiple pools. None inherits Net default.") diff --git a/netrun/src/tests/net/test_config.py b/netrun/src/tests/net/test_config.py index 1a7ef505..f85c2b89 100644 --- a/netrun/src/tests/net/test_config.py +++ b/netrun/src/tests/net/test_config.py @@ -2476,14 +2476,13 @@ def test_resolve_env_vars_missing_no_default(monkeypatch): # %% pts/tests/06_net/test_config.pct.py 263 def test_resolve_env_vars_nested_models(monkeypatch): - """PoolConfig containing ThreadPoolConfig with EnvVar.""" - monkeypatch.setenv("FLUSH_INTERVAL", "0.5") - cfg = PoolConfig( - print_flush_interval=EnvVar(env="FLUSH_INTERVAL"), - spec=ThreadPoolConfig(num_workers=2), + """NodeExecutionConfig with EnvVar in nested field.""" + monkeypatch.setenv("RETRY_WAIT", "2.5") + cfg = NodeExecutionConfig( + retry_wait=EnvVar(env="RETRY_WAIT"), ) resolved = cfg.resolve_env_vars() - assert resolved.print_flush_interval == 0.5 + assert resolved.retry_wait == 2.5 # %% pts/tests/06_net/test_config.pct.py 265 def test_resolve_env_vars_in_dict(monkeypatch): @@ -2536,15 +2535,14 @@ def test_resolve_env_vars_no_envvars_returns_self(): # %% pts/tests/06_net/test_config.pct.py 273 def test_netconfig_resolve_env_vars_before_imports(monkeypatch): - """Full NetConfig.resolve() with env var in dead_letter_callback.""" - monkeypatch.setenv("DL_CALLBACK", "json.loads") + """Full NetConfig.resolve() with env var in print_echo_stdout.""" + monkeypatch.setenv("ECHO_STDOUT", "false") nc = NetConfig( graph=GraphConfig(nodes=[]), - dead_letter_callback=EnvVar(env="DL_CALLBACK"), + print_echo_stdout=EnvVar(env="ECHO_STDOUT"), ) resolved = nc.resolve() - import json as json_mod - assert resolved.dead_letter_callback is json_mod.loads + assert resolved.print_echo_stdout is False # %% pts/tests/06_net/test_config.pct.py 275 def test_node_execution_config_resolve_with_env_var(monkeypatch): diff --git a/netrun/src/tests/net/test_net.py b/netrun/src/tests/net/test_net.py index 0d50e8d5..0d5c985e 100644 --- a/netrun/src/tests/net/test_net.py +++ b/netrun/src/tests/net/test_net.py @@ -1360,27 +1360,21 @@ def dummy(ctx, packets): # %% pts/tests/06_net/test_net.pct.py 132 def test_node_execution_config_new_fields(): - """Test NodeExecutionConfig has new print config fields.""" + """Test NodeExecutionConfig has config fields.""" config = NodeExecutionConfig( node_name="TestNode", - print_flush_interval=0.2, - print_buffer_max_size=100, print_echo_stdout=True, pool_allocation_method=RunAllocationMethod.LEAST_BUSY, ) - assert config.print_flush_interval == 0.2 - assert config.print_buffer_max_size == 100 assert config.print_echo_stdout is True assert config.pool_allocation_method == RunAllocationMethod.LEAST_BUSY # %% pts/tests/06_net/test_net.pct.py 134 def test_node_execution_config_defaults(): - """Test NodeExecutionConfig has correct defaults for new fields.""" + """Test NodeExecutionConfig has correct defaults.""" config = NodeExecutionConfig() - assert config.print_flush_interval == 0.1 - assert config.print_buffer_max_size is None assert config.print_echo_stdout is None assert config.pool_allocation_method is None From 35b5af87438b7780a5cafb996c6f397f4f5edb64 Mon Sep 17 00:00:00 2001 From: Lukas Kikuchi Date: Sat, 25 Apr 2026 21:28:59 +0100 Subject: [PATCH 06/15] test(netrun): add missing tests for Phase 2-3 features Adds 6 tests covering gaps identified in the feature test audit: - test_ctx_state_cleared_on_success: verify retry_state cleaned up after success - test_depends_on_multiple_dependencies: node C depends on both A and B - test_depends_on_none_is_noop: no depends_on works as before - test_resources_multi_slot: 2-slot resource allows bounded concurrency - test_resources_undefined_raises: referencing undefined resource errors - test_silence_detection_warns: worker silence triggers warning log Co-Authored-By: Claude Opus 4.6 (1M context) --- netrun/pts/tests/04_pool/test_thread.pct.py | 41 +++ netrun/pts/tests/06_net/test_net.pct.py | 305 ++++++++++++++++++++ netrun/src/tests/net/test_net.py | 284 +++++++++++++++++- netrun/src/tests/pool/test_thread.py | 36 ++- 4 files changed, 664 insertions(+), 2 deletions(-) diff --git a/netrun/pts/tests/04_pool/test_thread.pct.py b/netrun/pts/tests/04_pool/test_thread.pct.py index 544c88f3..8a247f49 100644 --- a/netrun/pts/tests/04_pool/test_thread.pct.py +++ b/netrun/pts/tests/04_pool/test_thread.pct.py @@ -366,3 +366,44 @@ async def test_concurrent_responses(): # %% await test_concurrent_responses(); + +# %% [markdown] +# ## Silence Detection + +# %% +#|export +@pytest.mark.asyncio +async def test_silence_detection_warns(caplog): + """Test that silence detection logs a warning for silent workers.""" + import logging + + def slow_echo_worker(channel, worker_id): + """Worker that responds slowly — triggers silence detection.""" + import time + try: + while True: + key, data = channel.recv() + time.sleep(0.1) # slow response + channel.send(f"echo:{key}", data) + except Exception: + pass + + # 1.0s silence timeout, monitor checks every 0.5s + pool = ThreadPool(slow_echo_worker, num_workers=1, silence_timeout=1.0) + async with pool: + # Send a message and receive to establish a baseline timestamp + await pool.send(0, "hello", "world") + msg = await pool.recv(timeout=5.0) + assert msg.key == "echo:hello" + + # Now wait >1s without any worker activity + with caplog.at_level(logging.WARNING, logger="netrun.pool"): + await asyncio.sleep(1.5) + + # Should have logged a silence warning + assert any("silent" in record.message.lower() for record in caplog.records), ( + f"Expected silence warning in logs, got: {[r.message for r in caplog.records]}" + ) + +# %% +await test_silence_detection_warns(); diff --git a/netrun/pts/tests/06_net/test_net.pct.py b/netrun/pts/tests/06_net/test_net.pct.py index b7af0c89..7faed875 100644 --- a/netrun/pts/tests/06_net/test_net.pct.py +++ b/netrun/pts/tests/06_net/test_net.pct.py @@ -7500,3 +7500,308 @@ def simple_node(ctx, packets): # %% asyncio.get_event_loop().run_until_complete(test_resources_none_is_noop()) + +# %% [markdown] +# ## Additional coverage tests + +# %% +#|export +@pytest.mark.asyncio +async def test_ctx_state_cleared_on_success(): + """Test that ctx.state is cleaned up from internal tracking after epoch succeeds.""" + def stateful_node(ctx, packets): + for port_name, pkt_ids in packets.items(): + for pid in pkt_ids: + ctx.consume_packet(pid) + ctx.state["key"] = "value" + + graph_config = GraphConfig( + nodes=[ + NodeConfig( + name="Stateful", + in_ports={"in": PortConfig()}, + in_salvo_conditions={ + "default": SalvoConditionConfig( + max_salvos=MaxSalvosFiniteConfig(max=1), + ports={"in": PacketCountAllConfig()}, + term=SalvoConditionTermPortConfig( + port_name="in", + state=PortStateNonEmptyConfig(), + ), + ), + }, + execution_config=NodeExecutionConfig( + node_name="Stateful", + pools=["main"], + exec_node_func=stateful_node, + ), + ), + ], + edges=[], + ) + config = NetConfig( + pools={"main": PoolConfig(spec=MainPoolConfig())}, + graph=graph_config, + ) + + async with Net(config) as net: + net.inject_data("Stateful", "in", [1]) + await net.run_until_blocked() + + # After success, the retry_state on the epoch record should be cleared + for epoch_id, record in net._epochs.items(): + if record.node_name == "Stateful": + assert record.retry_state == {}, ( + f"retry_state should be cleared on success, got {record.retry_state}" + ) + +# %% +asyncio.get_event_loop().run_until_complete(test_ctx_state_cleared_on_success()) + +# %% +#|export +@pytest.mark.asyncio +async def test_depends_on_multiple_dependencies(): + """Test that a node waits for ALL dependencies to complete.""" + execution_order = [] + + def node_a(ctx, packets): + for port_name, pkt_ids in packets.items(): + for pid in pkt_ids: + ctx.consume_packet(pid) + execution_order.append("A") + + def node_b(ctx, packets): + for port_name, pkt_ids in packets.items(): + for pid in pkt_ids: + ctx.consume_packet(pid) + execution_order.append("B") + + def node_c(ctx, packets): + for port_name, pkt_ids in packets.items(): + for pid in pkt_ids: + ctx.consume_packet(pid) + execution_order.append("C") + + def make_node(name, func, depends_on=None): + return NodeConfig( + name=name, + in_ports={"in": PortConfig()}, + in_salvo_conditions={ + "default": SalvoConditionConfig( + max_salvos=MaxSalvosFiniteConfig(max=1), + ports={"in": PacketCountAllConfig()}, + term=SalvoConditionTermPortConfig( + port_name="in", + state=PortStateNonEmptyConfig(), + ), + ), + }, + execution_config=NodeExecutionConfig( + node_name=name, + pools=["main"], + exec_node_func=func, + depends_on=depends_on, + ), + ) + + graph_config = GraphConfig( + nodes=[ + make_node("A", node_a), + make_node("B", node_b), + make_node("C", node_c, depends_on=["A", "B"]), + ], + edges=[], + ) + config = NetConfig( + pools={"main": PoolConfig(spec=MainPoolConfig())}, + graph=graph_config, + ) + + async with Net(config) as net: + net.inject_data("A", "in", [1]) + net.inject_data("B", "in", [2]) + net.inject_data("C", "in", [3]) + await net.run_until_blocked() + + # C depends on both A and B — it must run after both + assert "C" in execution_order + c_idx = execution_order.index("C") + assert "A" in execution_order[:c_idx], "A should complete before C" + assert "B" in execution_order[:c_idx], "B should complete before C" + +# %% +asyncio.get_event_loop().run_until_complete(test_depends_on_multiple_dependencies()) + +# %% +#|export +@pytest.mark.asyncio +async def test_depends_on_none_is_noop(): + """Test that nodes without depends_on work as before.""" + executed = [False] + + def simple_node(ctx, packets): + for port_name, pkt_ids in packets.items(): + for pid in pkt_ids: + ctx.consume_packet(pid) + executed[0] = True + + graph_config = GraphConfig( + nodes=[ + NodeConfig( + name="Simple", + in_ports={"in": PortConfig()}, + in_salvo_conditions={ + "default": SalvoConditionConfig( + max_salvos=MaxSalvosFiniteConfig(max=1), + ports={"in": PacketCountAllConfig()}, + term=SalvoConditionTermPortConfig( + port_name="in", + state=PortStateNonEmptyConfig(), + ), + ), + }, + execution_config=NodeExecutionConfig( + node_name="Simple", + pools=["main"], + exec_node_func=simple_node, + ), + ), + ], + edges=[], + ) + config = NetConfig( + pools={"main": PoolConfig(spec=MainPoolConfig())}, + graph=graph_config, + ) + + async with Net(config) as net: + net.inject_data("Simple", "in", [1]) + await net.run_until_blocked() + + assert executed[0] is True + +# %% +asyncio.get_event_loop().run_until_complete(test_depends_on_none_is_noop()) + +# %% +#|export +@pytest.mark.asyncio +async def test_resources_multi_slot(): + """Test that a multi-slot resource allows bounded concurrency.""" + import time as _time + + timestamps = {} + + async def gpu_node(ctx, packets): + name = ctx.node_name + for port_name, pkt_ids in packets.items(): + for pid in pkt_ids: + ctx.consume_packet(pid) + timestamps[f"{name}_start"] = _time.monotonic() + await asyncio.sleep(0.1) + timestamps[f"{name}_end"] = _time.monotonic() + + def make_gpu_node(name): + return NodeConfig( + name=name, + in_ports={"in": PortConfig()}, + in_salvo_conditions={ + "default": SalvoConditionConfig( + max_salvos=MaxSalvosFiniteConfig(max=1), + ports={"in": PacketCountAllConfig()}, + term=SalvoConditionTermPortConfig( + port_name="in", + state=PortStateNonEmptyConfig(), + ), + ), + }, + execution_config=NodeExecutionConfig( + node_name=name, + pools=["main"], + exec_node_func=gpu_node, + resources={"gpu": 1}, + ), + ) + + graph_config = GraphConfig( + nodes=[make_gpu_node("G1"), make_gpu_node("G2"), make_gpu_node("G3")], + edges=[], + ) + # 2 GPU slots: at most 2 of 3 nodes can run concurrently + config = NetConfig( + pools={"main": PoolConfig(spec=MainPoolConfig())}, + resources={"gpu": 2}, + graph=graph_config, + ) + + async with Net(config) as net: + net.inject_data("G1", "in", [1]) + net.inject_data("G2", "in", [2]) + net.inject_data("G3", "in", [3]) + await net.run_until_blocked() + + # All 3 should have completed + assert all(f"G{i}_end" in timestamps for i in range(1, 4)) + + # With 2 slots and 3 nodes of 0.1s each: total >= 0.2s (2 rounds) + # If only 1 slot: total >= 0.3s. If unlimited: total ~0.1s. + starts = [timestamps[f"G{i}_start"] for i in range(1, 4)] + ends = [timestamps[f"G{i}_end"] for i in range(1, 4)] + total = max(ends) - min(starts) + # Should take at least 0.15s (2 rounds of ~0.1s with some overlap) + # but less than 0.25s (which would indicate only 1 slot) + assert total >= 0.15, f"Expected >= 0.15s for 2-slot bounded concurrency, got {total:.3f}s" + +# %% +asyncio.get_event_loop().run_until_complete(test_resources_multi_slot()) + +# %% +#|export +@pytest.mark.asyncio +async def test_resources_undefined_raises(): + """Test that referencing an undefined resource raises an error.""" + def simple_node(ctx, packets): + for port_name, pkt_ids in packets.items(): + for pid in pkt_ids: + ctx.consume_packet(pid) + + graph_config = GraphConfig( + nodes=[ + NodeConfig( + name="BadNode", + in_ports={"in": PortConfig()}, + in_salvo_conditions={ + "default": SalvoConditionConfig( + max_salvos=MaxSalvosFiniteConfig(max=1), + ports={"in": PacketCountAllConfig()}, + term=SalvoConditionTermPortConfig( + port_name="in", + state=PortStateNonEmptyConfig(), + ), + ), + }, + execution_config=NodeExecutionConfig( + node_name="BadNode", + pools=["main"], + exec_node_func=simple_node, + resources={"nonexistent_resource": 1}, + ), + ), + ], + edges=[], + ) + # No resources defined at net level, but node requires one + config = NetConfig( + pools={"main": PoolConfig(spec=MainPoolConfig())}, + resources={"other_resource": 1}, # different name + graph=graph_config, + ) + + async with Net(config) as net: + net.inject_data("BadNode", "in", [1]) + with pytest.raises(ValueError, match="nonexistent_resource"): + await net.run_until_blocked() + +# %% +asyncio.get_event_loop().run_until_complete(test_resources_undefined_raises()) diff --git a/netrun/src/tests/net/test_net.py b/netrun/src/tests/net/test_net.py index 0d5c985e..c6a15e8a 100644 --- a/netrun/src/tests/net/test_net.py +++ b/netrun/src/tests/net/test_net.py @@ -1,6 +1,6 @@ # AUTOGENERATED! DO NOT EDIT! File to edit: pts/tests/06_net/test_net.pct.py -__all__ = ['create_simple_graph_config', 'create_simple_net_config', 'test_async_epoch_callback', 'test_async_error_on_thread_pool', 'test_async_exec_func_on_main_pool', 'test_async_exec_func_on_thread_pool', 'test_async_from_function_factory_on_thread_pool', 'test_async_retry_on_thread_pool', 'test_async_subprocess_on_thread_pool', 'test_check_type_distinguishes_modes', 'test_check_type_string_generic_fails', 'test_check_type_string_simple', 'test_check_type_type_object_generic', 'test_check_type_type_object_generic_mismatch', 'test_check_type_type_object_simple', 'test_close_node_func_called_on_net_stop', 'test_concurrent_async_siblings_on_main_pool', 'test_context_cancel_epoch', 'test_context_consume_packet', 'test_context_consume_packet_not_found', 'test_context_create_packet', 'test_context_create_packet_from_value_func', 'test_context_full_workflow', 'test_context_get_execution_result', 'test_context_load_output_port', 'test_context_print_accumulates', 'test_context_print_basic', 'test_context_print_custom_separators', 'test_context_print_echo_stdout', 'test_context_print_empty', 'test_context_print_flush_ignored', 'test_context_print_multiple_timestamps', 'test_context_print_non_string', 'test_context_send_output_salvo', 'test_create_func_preprocessor_from_config', 'test_create_func_preprocessor_from_config_empty', 'test_create_net_func_preprocessor_basic', 'test_create_net_func_preprocessor_captures_exception', 'test_create_net_func_preprocessor_captures_prints', 'test_create_net_func_preprocessor_with_retry_info', 'test_ctx_state_empty_by_default', 'test_ctx_state_persists_across_retries', 'test_ctx_vars_access', 'test_ctx_vars_empty', 'test_ctx_vars_inherit_missing_global_raises', 'test_ctx_vars_merging', 'test_ctx_vars_merging_with_inherit', 'test_dead_letter_queue_after_max_retries', 'test_dead_letter_queue_has_pool_and_worker', 'test_defer_init_delays_init_node_func', 'test_deferred_actions_preserved_in_result', 'test_deferred_queue_add_consume_packet', 'test_deferred_queue_add_create_packet', 'test_deferred_queue_add_load_output_port', 'test_deferred_queue_add_send_output_salvo', 'test_deferred_queue_discard', 'test_deferred_queue_multiple_creates', 'test_depends_on_blocks_until_dependency_completes', 'test_enable_disable_unknown_node', 'test_epoch_callback_deregistration', 'test_epoch_cancelled_can_be_raised_and_caught', 'test_epoch_cancelled_exception', 'test_epoch_end_on_cancelled', 'test_epoch_error_basic', 'test_epoch_error_can_be_caught', 'test_epoch_error_chaining', 'test_epoch_error_minimal', 'test_epoch_error_str', 'test_epoch_execution_simple_node', 'test_epoch_execution_with_output', 'test_epoch_record_cancellation_lifecycle', 'test_epoch_record_lifecycle_timestamps', 'test_epoch_record_out_salvos_populated', 'test_exception_queue_contains_epoch_error', 'test_execute_node_calls_init_node_func', 'test_execute_node_inside_net_basic', 'test_execute_node_inside_net_source_node', 'test_execute_node_outside_net', 'test_execute_node_outside_net_async_func', 'test_execute_node_salvo_condition_not_satisfied', 'test_execution_manager_get_pool', 'test_factory_node_exec_func_override', 'test_faulthandler_enabled_after_init', 'test_global_max_epochs_default_unlimited', 'test_global_max_epochs_limits_all_nodes', 'test_init_node_func_called_on_net_start', 'test_init_node_func_called_once_with_defer_init', 'test_is_blocked_with_disabled_node', 'test_max_epochs_allows_up_to_limit', 'test_max_epochs_exceeded_exception', 'test_max_epochs_none_unlimited', 'test_max_epochs_one_raises_on_second', 'test_max_epochs_queued_when_not_propagating', 'test_max_parallel_epochs_limits_concurrent_starts', 'test_max_parallel_epochs_none_allows_all', 'test_max_parallel_epochs_per_node', 'test_multiple_epoch_callbacks', 'test_net_check_rate_limit_enforced', 'test_net_check_rate_limit_no_config', 'test_net_check_rate_limit_none_limit', 'test_net_check_rate_limit_window_expires', 'test_net_clear_dead_letter_queue', 'test_net_close_sync', 'test_net_config_default_pool_allocation_method', 'test_net_config_default_pool_allocation_method_default', 'test_net_config_property', 'test_net_config_type_checking', 'test_net_context_manager', 'test_net_creation', 'test_net_dead_letter_queue_empty', 'test_net_dead_letter_queue_returns_copy', 'test_net_from_file_json', 'test_net_from_file_not_found', 'test_net_from_file_unsupported_format', 'test_net_get_epoch_log_empty', 'test_net_get_node_logs_empty', 'test_net_get_startable_epochs', 'test_net_graph_property', 'test_net_handle_print_buffer', 'test_net_init_sync', 'test_net_install_sigint_handler', 'test_net_invalid_pool_type_raises', 'test_net_is_blocked_empty_network', 'test_net_is_blocked_with_running_epochs', 'test_net_pause_and_resume', 'test_net_protocol_keys_uniqueness', 'test_net_protocol_keys_values', 'test_net_run_step', 'test_net_run_until_blocked', 'test_net_running_epochs_empty', 'test_net_start_and_stop', 'test_net_start_background', 'test_net_start_background_already_running', 'test_net_start_twice_raises', 'test_net_with_multiple_pool_types', 'test_net_with_node_execution_configs', 'test_node_disable_at_runtime', 'test_node_disabled_via_config', 'test_node_enable_at_runtime', 'test_node_execution_config_defaults', 'test_node_execution_config_max_epochs_default', 'test_node_execution_config_max_epochs_set', 'test_node_execution_config_new_fields', 'test_node_execution_config_retry_defaults', 'test_node_execution_config_type_checking', 'test_node_execution_config_with_retries', 'test_node_execution_context_creation', 'test_node_execution_context_with_retry_info', 'test_node_execution_result_with_exception', 'test_node_execution_result_with_func_result', 'test_node_failure_context_creation', 'test_node_failure_context_full', 'test_node_failure_context_has_pool_and_worker', 'test_node_failure_context_print', 'test_node_level_override_print', 'test_node_level_override_propagate', 'test_node_scoped_epoch_callbacks', 'test_on_epoch_end_callback', 'test_on_epoch_start_callback', 'test_on_node_failure_callback', 'test_per_node_max_epochs_overrides_global', 'test_per_node_minus_one_overrides_global_limit', 'test_pool_server_context_as_context_manager_still_works', 'test_pool_server_context_log_no_file', 'test_pool_server_context_start_stop', 'test_pool_server_context_start_stop_with_log_file', 'test_preprocessor_handles_cancel_epoch', 'test_preprocessor_type_checking_inheritance', 'test_print_exceptions_includes_pool_worker', 'test_print_exceptions_true_prints_to_stderr', 'test_propagate_exceptions_false_queues', 'test_propagate_exceptions_true_raises', 'test_propagate_exceptions_wraps_in_epoch_error', 'test_request_pool_shutdown_end_to_end', 'test_request_pool_shutdown_non_remote_raises', 'test_resources_mutual_exclusion', 'test_resources_none_is_noop', 'test_retry_calls_gc_collect', 'test_retry_on_failure', 'test_run_on_init_basic', 'test_run_on_init_disabled', 'test_run_on_init_error_no_valid_condition', 'test_run_on_init_outputs_flow_downstream', 'test_run_on_init_skip_via_constructor', 'test_run_on_init_skip_via_start_param', 'test_run_on_init_start_param_overrides_constructor', 'test_serve_pool_accepts_path', 'test_serve_pool_accepts_pathlib_path', 'test_serve_pool_custom_worker_name', 'test_serve_pool_end_to_end_with_net', 'test_serve_pool_log_file_writes', 'test_serve_pool_no_log_file', 'test_serve_pool_returns_pool_server_context', 'test_serve_pool_starts_and_stops_server', 'test_serve_pool_with_log_file', 'test_server_log_callback', 'test_start_stop_with_async_funcs', 'test_stop_not_called_for_unstarted_deferred_node', 'test_streaming_downstream_starts_before_slow_sibling', 'test_streaming_error_propagates', 'test_streaming_linear_pipeline', 'test_streaming_max_parallel_epochs_across_rounds', 'test_timeout_enforcement_raises_epoch_error', 'test_timeout_goes_to_dead_letter_queue', 'test_timeout_none_no_limit', 'test_type_checking_disabled'] +__all__ = ['create_simple_graph_config', 'create_simple_net_config', 'test_async_epoch_callback', 'test_async_error_on_thread_pool', 'test_async_exec_func_on_main_pool', 'test_async_exec_func_on_thread_pool', 'test_async_from_function_factory_on_thread_pool', 'test_async_retry_on_thread_pool', 'test_async_subprocess_on_thread_pool', 'test_check_type_distinguishes_modes', 'test_check_type_string_generic_fails', 'test_check_type_string_simple', 'test_check_type_type_object_generic', 'test_check_type_type_object_generic_mismatch', 'test_check_type_type_object_simple', 'test_close_node_func_called_on_net_stop', 'test_concurrent_async_siblings_on_main_pool', 'test_context_cancel_epoch', 'test_context_consume_packet', 'test_context_consume_packet_not_found', 'test_context_create_packet', 'test_context_create_packet_from_value_func', 'test_context_full_workflow', 'test_context_get_execution_result', 'test_context_load_output_port', 'test_context_print_accumulates', 'test_context_print_basic', 'test_context_print_custom_separators', 'test_context_print_echo_stdout', 'test_context_print_empty', 'test_context_print_flush_ignored', 'test_context_print_multiple_timestamps', 'test_context_print_non_string', 'test_context_send_output_salvo', 'test_create_func_preprocessor_from_config', 'test_create_func_preprocessor_from_config_empty', 'test_create_net_func_preprocessor_basic', 'test_create_net_func_preprocessor_captures_exception', 'test_create_net_func_preprocessor_captures_prints', 'test_create_net_func_preprocessor_with_retry_info', 'test_ctx_state_cleared_on_success', 'test_ctx_state_empty_by_default', 'test_ctx_state_persists_across_retries', 'test_ctx_vars_access', 'test_ctx_vars_empty', 'test_ctx_vars_inherit_missing_global_raises', 'test_ctx_vars_merging', 'test_ctx_vars_merging_with_inherit', 'test_dead_letter_queue_after_max_retries', 'test_dead_letter_queue_has_pool_and_worker', 'test_defer_init_delays_init_node_func', 'test_deferred_actions_preserved_in_result', 'test_deferred_queue_add_consume_packet', 'test_deferred_queue_add_create_packet', 'test_deferred_queue_add_load_output_port', 'test_deferred_queue_add_send_output_salvo', 'test_deferred_queue_discard', 'test_deferred_queue_multiple_creates', 'test_depends_on_blocks_until_dependency_completes', 'test_depends_on_multiple_dependencies', 'test_depends_on_none_is_noop', 'test_enable_disable_unknown_node', 'test_epoch_callback_deregistration', 'test_epoch_cancelled_can_be_raised_and_caught', 'test_epoch_cancelled_exception', 'test_epoch_end_on_cancelled', 'test_epoch_error_basic', 'test_epoch_error_can_be_caught', 'test_epoch_error_chaining', 'test_epoch_error_minimal', 'test_epoch_error_str', 'test_epoch_execution_simple_node', 'test_epoch_execution_with_output', 'test_epoch_record_cancellation_lifecycle', 'test_epoch_record_lifecycle_timestamps', 'test_epoch_record_out_salvos_populated', 'test_exception_queue_contains_epoch_error', 'test_execute_node_calls_init_node_func', 'test_execute_node_inside_net_basic', 'test_execute_node_inside_net_source_node', 'test_execute_node_outside_net', 'test_execute_node_outside_net_async_func', 'test_execute_node_salvo_condition_not_satisfied', 'test_execution_manager_get_pool', 'test_factory_node_exec_func_override', 'test_faulthandler_enabled_after_init', 'test_global_max_epochs_default_unlimited', 'test_global_max_epochs_limits_all_nodes', 'test_init_node_func_called_on_net_start', 'test_init_node_func_called_once_with_defer_init', 'test_is_blocked_with_disabled_node', 'test_max_epochs_allows_up_to_limit', 'test_max_epochs_exceeded_exception', 'test_max_epochs_none_unlimited', 'test_max_epochs_one_raises_on_second', 'test_max_epochs_queued_when_not_propagating', 'test_max_parallel_epochs_limits_concurrent_starts', 'test_max_parallel_epochs_none_allows_all', 'test_max_parallel_epochs_per_node', 'test_multiple_epoch_callbacks', 'test_net_check_rate_limit_enforced', 'test_net_check_rate_limit_no_config', 'test_net_check_rate_limit_none_limit', 'test_net_check_rate_limit_window_expires', 'test_net_clear_dead_letter_queue', 'test_net_close_sync', 'test_net_config_default_pool_allocation_method', 'test_net_config_default_pool_allocation_method_default', 'test_net_config_property', 'test_net_config_type_checking', 'test_net_context_manager', 'test_net_creation', 'test_net_dead_letter_queue_empty', 'test_net_dead_letter_queue_returns_copy', 'test_net_from_file_json', 'test_net_from_file_not_found', 'test_net_from_file_unsupported_format', 'test_net_get_epoch_log_empty', 'test_net_get_node_logs_empty', 'test_net_get_startable_epochs', 'test_net_graph_property', 'test_net_handle_print_buffer', 'test_net_init_sync', 'test_net_install_sigint_handler', 'test_net_invalid_pool_type_raises', 'test_net_is_blocked_empty_network', 'test_net_is_blocked_with_running_epochs', 'test_net_pause_and_resume', 'test_net_protocol_keys_uniqueness', 'test_net_protocol_keys_values', 'test_net_run_step', 'test_net_run_until_blocked', 'test_net_running_epochs_empty', 'test_net_start_and_stop', 'test_net_start_background', 'test_net_start_background_already_running', 'test_net_start_twice_raises', 'test_net_with_multiple_pool_types', 'test_net_with_node_execution_configs', 'test_node_disable_at_runtime', 'test_node_disabled_via_config', 'test_node_enable_at_runtime', 'test_node_execution_config_defaults', 'test_node_execution_config_max_epochs_default', 'test_node_execution_config_max_epochs_set', 'test_node_execution_config_new_fields', 'test_node_execution_config_retry_defaults', 'test_node_execution_config_type_checking', 'test_node_execution_config_with_retries', 'test_node_execution_context_creation', 'test_node_execution_context_with_retry_info', 'test_node_execution_result_with_exception', 'test_node_execution_result_with_func_result', 'test_node_failure_context_creation', 'test_node_failure_context_full', 'test_node_failure_context_has_pool_and_worker', 'test_node_failure_context_print', 'test_node_level_override_print', 'test_node_level_override_propagate', 'test_node_scoped_epoch_callbacks', 'test_on_epoch_end_callback', 'test_on_epoch_start_callback', 'test_on_node_failure_callback', 'test_per_node_max_epochs_overrides_global', 'test_per_node_minus_one_overrides_global_limit', 'test_pool_server_context_as_context_manager_still_works', 'test_pool_server_context_log_no_file', 'test_pool_server_context_start_stop', 'test_pool_server_context_start_stop_with_log_file', 'test_preprocessor_handles_cancel_epoch', 'test_preprocessor_type_checking_inheritance', 'test_print_exceptions_includes_pool_worker', 'test_print_exceptions_true_prints_to_stderr', 'test_propagate_exceptions_false_queues', 'test_propagate_exceptions_true_raises', 'test_propagate_exceptions_wraps_in_epoch_error', 'test_request_pool_shutdown_end_to_end', 'test_request_pool_shutdown_non_remote_raises', 'test_resources_multi_slot', 'test_resources_mutual_exclusion', 'test_resources_none_is_noop', 'test_resources_undefined_raises', 'test_retry_calls_gc_collect', 'test_retry_on_failure', 'test_run_on_init_basic', 'test_run_on_init_disabled', 'test_run_on_init_error_no_valid_condition', 'test_run_on_init_outputs_flow_downstream', 'test_run_on_init_skip_via_constructor', 'test_run_on_init_skip_via_start_param', 'test_run_on_init_start_param_overrides_constructor', 'test_serve_pool_accepts_path', 'test_serve_pool_accepts_pathlib_path', 'test_serve_pool_custom_worker_name', 'test_serve_pool_end_to_end_with_net', 'test_serve_pool_log_file_writes', 'test_serve_pool_no_log_file', 'test_serve_pool_returns_pool_server_context', 'test_serve_pool_starts_and_stops_server', 'test_serve_pool_with_log_file', 'test_server_log_callback', 'test_start_stop_with_async_funcs', 'test_stop_not_called_for_unstarted_deferred_node', 'test_streaming_downstream_starts_before_slow_sibling', 'test_streaming_error_propagates', 'test_streaming_linear_pipeline', 'test_streaming_max_parallel_epochs_across_rounds', 'test_timeout_enforcement_raises_epoch_error', 'test_timeout_goes_to_dead_letter_queue', 'test_timeout_none_no_limit', 'test_type_checking_disabled'] # %% pts/tests/06_net/test_net.pct.py 2 import pytest @@ -6790,3 +6790,285 @@ def simple_node(ctx, packets): await net.run_until_blocked() assert executed[0] is True + +# %% pts/tests/06_net/test_net.pct.py 373 +@pytest.mark.asyncio +async def test_ctx_state_cleared_on_success(): + """Test that ctx.state is cleaned up from internal tracking after epoch succeeds.""" + def stateful_node(ctx, packets): + for port_name, pkt_ids in packets.items(): + for pid in pkt_ids: + ctx.consume_packet(pid) + ctx.state["key"] = "value" + + graph_config = GraphConfig( + nodes=[ + NodeConfig( + name="Stateful", + in_ports={"in": PortConfig()}, + in_salvo_conditions={ + "default": SalvoConditionConfig( + max_salvos=MaxSalvosFiniteConfig(max=1), + ports={"in": PacketCountAllConfig()}, + term=SalvoConditionTermPortConfig( + port_name="in", + state=PortStateNonEmptyConfig(), + ), + ), + }, + execution_config=NodeExecutionConfig( + node_name="Stateful", + pools=["main"], + exec_node_func=stateful_node, + ), + ), + ], + edges=[], + ) + config = NetConfig( + pools={"main": PoolConfig(spec=MainPoolConfig())}, + graph=graph_config, + ) + + async with Net(config) as net: + net.inject_data("Stateful", "in", [1]) + await net.run_until_blocked() + + # After success, the retry_state on the epoch record should be cleared + for epoch_id, record in net._epochs.items(): + if record.node_name == "Stateful": + assert record.retry_state == {}, ( + f"retry_state should be cleared on success, got {record.retry_state}" + ) + +# %% pts/tests/06_net/test_net.pct.py 375 +@pytest.mark.asyncio +async def test_depends_on_multiple_dependencies(): + """Test that a node waits for ALL dependencies to complete.""" + execution_order = [] + + def node_a(ctx, packets): + for port_name, pkt_ids in packets.items(): + for pid in pkt_ids: + ctx.consume_packet(pid) + execution_order.append("A") + + def node_b(ctx, packets): + for port_name, pkt_ids in packets.items(): + for pid in pkt_ids: + ctx.consume_packet(pid) + execution_order.append("B") + + def node_c(ctx, packets): + for port_name, pkt_ids in packets.items(): + for pid in pkt_ids: + ctx.consume_packet(pid) + execution_order.append("C") + + def make_node(name, func, depends_on=None): + return NodeConfig( + name=name, + in_ports={"in": PortConfig()}, + in_salvo_conditions={ + "default": SalvoConditionConfig( + max_salvos=MaxSalvosFiniteConfig(max=1), + ports={"in": PacketCountAllConfig()}, + term=SalvoConditionTermPortConfig( + port_name="in", + state=PortStateNonEmptyConfig(), + ), + ), + }, + execution_config=NodeExecutionConfig( + node_name=name, + pools=["main"], + exec_node_func=func, + depends_on=depends_on, + ), + ) + + graph_config = GraphConfig( + nodes=[ + make_node("A", node_a), + make_node("B", node_b), + make_node("C", node_c, depends_on=["A", "B"]), + ], + edges=[], + ) + config = NetConfig( + pools={"main": PoolConfig(spec=MainPoolConfig())}, + graph=graph_config, + ) + + async with Net(config) as net: + net.inject_data("A", "in", [1]) + net.inject_data("B", "in", [2]) + net.inject_data("C", "in", [3]) + await net.run_until_blocked() + + # C depends on both A and B — it must run after both + assert "C" in execution_order + c_idx = execution_order.index("C") + assert "A" in execution_order[:c_idx], "A should complete before C" + assert "B" in execution_order[:c_idx], "B should complete before C" + +# %% pts/tests/06_net/test_net.pct.py 377 +@pytest.mark.asyncio +async def test_depends_on_none_is_noop(): + """Test that nodes without depends_on work as before.""" + executed = [False] + + def simple_node(ctx, packets): + for port_name, pkt_ids in packets.items(): + for pid in pkt_ids: + ctx.consume_packet(pid) + executed[0] = True + + graph_config = GraphConfig( + nodes=[ + NodeConfig( + name="Simple", + in_ports={"in": PortConfig()}, + in_salvo_conditions={ + "default": SalvoConditionConfig( + max_salvos=MaxSalvosFiniteConfig(max=1), + ports={"in": PacketCountAllConfig()}, + term=SalvoConditionTermPortConfig( + port_name="in", + state=PortStateNonEmptyConfig(), + ), + ), + }, + execution_config=NodeExecutionConfig( + node_name="Simple", + pools=["main"], + exec_node_func=simple_node, + ), + ), + ], + edges=[], + ) + config = NetConfig( + pools={"main": PoolConfig(spec=MainPoolConfig())}, + graph=graph_config, + ) + + async with Net(config) as net: + net.inject_data("Simple", "in", [1]) + await net.run_until_blocked() + + assert executed[0] is True + +# %% pts/tests/06_net/test_net.pct.py 379 +@pytest.mark.asyncio +async def test_resources_multi_slot(): + """Test that a multi-slot resource allows bounded concurrency.""" + import time as _time + + timestamps = {} + + async def gpu_node(ctx, packets): + name = ctx.node_name + for port_name, pkt_ids in packets.items(): + for pid in pkt_ids: + ctx.consume_packet(pid) + timestamps[f"{name}_start"] = _time.monotonic() + await asyncio.sleep(0.1) + timestamps[f"{name}_end"] = _time.monotonic() + + def make_gpu_node(name): + return NodeConfig( + name=name, + in_ports={"in": PortConfig()}, + in_salvo_conditions={ + "default": SalvoConditionConfig( + max_salvos=MaxSalvosFiniteConfig(max=1), + ports={"in": PacketCountAllConfig()}, + term=SalvoConditionTermPortConfig( + port_name="in", + state=PortStateNonEmptyConfig(), + ), + ), + }, + execution_config=NodeExecutionConfig( + node_name=name, + pools=["main"], + exec_node_func=gpu_node, + resources={"gpu": 1}, + ), + ) + + graph_config = GraphConfig( + nodes=[make_gpu_node("G1"), make_gpu_node("G2"), make_gpu_node("G3")], + edges=[], + ) + # 2 GPU slots: at most 2 of 3 nodes can run concurrently + config = NetConfig( + pools={"main": PoolConfig(spec=MainPoolConfig())}, + resources={"gpu": 2}, + graph=graph_config, + ) + + async with Net(config) as net: + net.inject_data("G1", "in", [1]) + net.inject_data("G2", "in", [2]) + net.inject_data("G3", "in", [3]) + await net.run_until_blocked() + + # All 3 should have completed + assert all(f"G{i}_end" in timestamps for i in range(1, 4)) + + # With 2 slots and 3 nodes of 0.1s each: total >= 0.2s (2 rounds) + # If only 1 slot: total >= 0.3s. If unlimited: total ~0.1s. + starts = [timestamps[f"G{i}_start"] for i in range(1, 4)] + ends = [timestamps[f"G{i}_end"] for i in range(1, 4)] + total = max(ends) - min(starts) + # Should take at least 0.15s (2 rounds of ~0.1s with some overlap) + # but less than 0.25s (which would indicate only 1 slot) + assert total >= 0.15, f"Expected >= 0.15s for 2-slot bounded concurrency, got {total:.3f}s" + +# %% pts/tests/06_net/test_net.pct.py 381 +@pytest.mark.asyncio +async def test_resources_undefined_raises(): + """Test that referencing an undefined resource raises an error.""" + def simple_node(ctx, packets): + for port_name, pkt_ids in packets.items(): + for pid in pkt_ids: + ctx.consume_packet(pid) + + graph_config = GraphConfig( + nodes=[ + NodeConfig( + name="BadNode", + in_ports={"in": PortConfig()}, + in_salvo_conditions={ + "default": SalvoConditionConfig( + max_salvos=MaxSalvosFiniteConfig(max=1), + ports={"in": PacketCountAllConfig()}, + term=SalvoConditionTermPortConfig( + port_name="in", + state=PortStateNonEmptyConfig(), + ), + ), + }, + execution_config=NodeExecutionConfig( + node_name="BadNode", + pools=["main"], + exec_node_func=simple_node, + resources={"nonexistent_resource": 1}, + ), + ), + ], + edges=[], + ) + # No resources defined at net level, but node requires one + config = NetConfig( + pools={"main": PoolConfig(spec=MainPoolConfig())}, + resources={"other_resource": 1}, # different name + graph=graph_config, + ) + + async with Net(config) as net: + net.inject_data("BadNode", "in", [1]) + with pytest.raises(ValueError, match="nonexistent_resource"): + await net.run_until_blocked() diff --git a/netrun/src/tests/pool/test_thread.py b/netrun/src/tests/pool/test_thread.py index bd3da758..cd61624f 100644 --- a/netrun/src/tests/pool/test_thread.py +++ b/netrun/src/tests/pool/test_thread.py @@ -1,6 +1,6 @@ # AUTOGENERATED! DO NOT EDIT! File to edit: pts/tests/04_pool/test_thread.pct.py -__all__ = ['compute_worker', 'echo_worker', 'slow_worker', 'test_broadcast', 'test_compute_workers', 'test_concurrent_responses', 'test_context_manager', 'test_double_start_raises', 'test_invalid_worker_id', 'test_pool_creation', 'test_pool_invalid_num_workers', 'test_recv_before_start_raises', 'test_recv_timeout', 'test_send_before_start_raises', 'test_send_recv_multiple_messages_same_worker', 'test_send_recv_multiple_workers', 'test_send_recv_single', 'test_start_and_close', 'test_try_recv_empty', 'test_try_recv_with_message'] +__all__ = ['compute_worker', 'echo_worker', 'slow_worker', 'test_broadcast', 'test_compute_workers', 'test_concurrent_responses', 'test_context_manager', 'test_double_start_raises', 'test_invalid_worker_id', 'test_pool_creation', 'test_pool_invalid_num_workers', 'test_recv_before_start_raises', 'test_recv_timeout', 'test_send_before_start_raises', 'test_send_recv_multiple_messages_same_worker', 'test_send_recv_multiple_workers', 'test_send_recv_single', 'test_silence_detection_warns', 'test_start_and_close', 'test_try_recv_empty', 'test_try_recv_with_message'] # %% pts/tests/04_pool/test_thread.pct.py 2 import pytest @@ -254,3 +254,37 @@ async def test_concurrent_responses(): responses.append(msg.worker_id) assert sorted(responses) == [0, 1, 2, 3] + +# %% pts/tests/04_pool/test_thread.pct.py 51 +@pytest.mark.asyncio +async def test_silence_detection_warns(caplog): + """Test that silence detection logs a warning for silent workers.""" + import logging + + def slow_echo_worker(channel, worker_id): + """Worker that responds slowly — triggers silence detection.""" + import time + try: + while True: + key, data = channel.recv() + time.sleep(0.1) # slow response + channel.send(f"echo:{key}", data) + except Exception: + pass + + # 1.0s silence timeout, monitor checks every 0.5s + pool = ThreadPool(slow_echo_worker, num_workers=1, silence_timeout=1.0) + async with pool: + # Send a message and receive to establish a baseline timestamp + await pool.send(0, "hello", "world") + msg = await pool.recv(timeout=5.0) + assert msg.key == "echo:hello" + + # Now wait >1s without any worker activity + with caplog.at_level(logging.WARNING, logger="netrun.pool"): + await asyncio.sleep(1.5) + + # Should have logged a silence warning + assert any("silent" in record.message.lower() for record in caplog.records), ( + f"Expected silence warning in logs, got: {[r.message for r in caplog.records]}" + ) From b5b3d0839196cecaebe2c530bb0fa4d35f5bc8f1 Mon Sep 17 00:00:00 2001 From: Lukas Kikuchi Date: Mon, 27 Apr 2026 15:38:32 +0100 Subject: [PATCH 07/15] fix(netrun): validate depends_on for cycles and invalid references MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds config-time validation to catch depends_on misconfigurations that would otherwise cause silent deadlocks: - Circular dependency detection via topological sort in Net.__init__ (A depends_on B, B depends_on A → ValueError with clear message) - Invalid node name detection (depends_on non-existent node → ValueError) - Updated field docstrings with edge case documentation Co-Authored-By: Claude Opus 4.6 (1M context) --- .../netrun/06_net/00_config/01_nodes.pct.py | 4 +- netrun/pts/netrun/06_net/01_net/02_net.pct.py | 30 ++++++ netrun/pts/tests/06_net/test_net.pct.py | 93 +++++++++++++++++++ netrun/src/netrun/net/_net/_net.py | 30 ++++++ netrun/src/netrun/net/config/_nodes.py | 4 +- netrun/src/tests/net/test_net.py | 84 ++++++++++++++++- 6 files changed, 240 insertions(+), 5 deletions(-) diff --git a/netrun/pts/netrun/06_net/00_config/01_nodes.pct.py b/netrun/pts/netrun/06_net/00_config/01_nodes.pct.py index 43238256..56a3d3a2 100644 --- a/netrun/pts/netrun/06_net/00_config/01_nodes.pct.py +++ b/netrun/pts/netrun/06_net/00_config/01_nodes.pct.py @@ -267,9 +267,9 @@ class NodeExecutionConfig(VarResolvableModel): rate_limit_per_second: float | VarRef | None = Field(default=None, description="Maximum epoch triggers per second.") - depends_on: list[str] | None = Field(default=None, description="Node names that must have completed at least one epoch before this node can start. Provides directional ordering without explicit control edges.") + depends_on: list[str] | None = Field(default=None, description="Node names that must have completed at least one epoch before this node can start. Provides directional ordering without explicit control edges. Must form a DAG — circular dependencies raise ValueError at Net construction. Non-existent node names also raise ValueError.") - resources: dict[str, int] | None = Field(default=None, description="Resource requirements as {resource_name: slots_needed}. Resources must be defined in NetConfig.resources with their capacity. Node epoch starts only when all required resource slots are available.") + resources: dict[str, int] | None = Field(default=None, description="Resource requirements as {resource_name: slots_needed}. Resources must be defined in NetConfig.resources with their capacity. Node epoch starts only when all required resource slots are available. Slots are acquired when epoch starts and released when epoch finishes, fails, or is cancelled.") retries: int | VarRef | None = Field(default=None, description="Number of retry attempts on failure. None inherits from NetConfig.") retry_wait: float | VarRef | None = Field(default=None, description="Wait time in seconds between retries. None inherits from NetConfig.") diff --git a/netrun/pts/netrun/06_net/01_net/02_net.pct.py b/netrun/pts/netrun/06_net/01_net/02_net.pct.py index e8335bc4..ab8e8c86 100644 --- a/netrun/pts/netrun/06_net/01_net/02_net.pct.py +++ b/netrun/pts/netrun/06_net/01_net/02_net.pct.py @@ -470,6 +470,36 @@ def __init__(self, config: NetConfig, run_init_nodes: bool = True): if node_config.execution_config is not None: self._node_execution_configs[node_config.name] = node_config.execution_config + # Validate depends_on: check for cycles and invalid references + all_node_names = {nc.name for nc in self._config_resolved.graph.nodes} + for node_name, exec_config in self._node_execution_configs.items(): + if exec_config.depends_on: + for dep in exec_config.depends_on: + if dep not in all_node_names: + raise ValueError( + f"Node '{node_name}' depends_on '{dep}' which does not exist in the graph" + ) + # Topological cycle check for depends_on + visited: set[str] = set() + path: set[str] = set() + def _check_cycle(node: str) -> None: + if node in path: + raise ValueError( + f"Circular dependency detected involving node '{node}'. " + f"depends_on must form a DAG (no cycles)." + ) + if node in visited: + return + path.add(node) + config = self._node_execution_configs.get(node) + if config and config.depends_on: + for dep in config.depends_on: + _check_cycle(dep) + path.remove(node) + visited.add(node) + for node_name in self._node_execution_configs: + _check_cycle(node_name) + # Disabled nodes set (populated from config, mutated at runtime) self._disabled_nodes: set[str] = set() for node_name, exec_config in self._node_execution_configs.items(): diff --git a/netrun/pts/tests/06_net/test_net.pct.py b/netrun/pts/tests/06_net/test_net.pct.py index 7faed875..80843ba2 100644 --- a/netrun/pts/tests/06_net/test_net.pct.py +++ b/netrun/pts/tests/06_net/test_net.pct.py @@ -7805,3 +7805,96 @@ def simple_node(ctx, packets): # %% asyncio.get_event_loop().run_until_complete(test_resources_undefined_raises()) + +# %% [markdown] +# ## Validation tests + +# %% +#|export +def test_depends_on_circular_raises(): + """Test that circular depends_on raises ValueError at construction.""" + def noop(ctx, packets): + pass + + def make_node(name, depends_on=None): + return NodeConfig( + name=name, + in_ports={"in": PortConfig()}, + in_salvo_conditions={ + "default": SalvoConditionConfig( + max_salvos=MaxSalvosFiniteConfig(max=1), + ports={"in": PacketCountAllConfig()}, + term=SalvoConditionTermPortConfig( + port_name="in", + state=PortStateNonEmptyConfig(), + ), + ), + }, + execution_config=NodeExecutionConfig( + node_name=name, + pools=["main"], + exec_node_func=noop, + depends_on=depends_on, + ), + ) + + graph_config = GraphConfig( + nodes=[ + make_node("A", depends_on=["B"]), + make_node("B", depends_on=["A"]), + ], + edges=[], + ) + config = NetConfig( + pools={"main": PoolConfig(spec=MainPoolConfig())}, + graph=graph_config, + ) + + with pytest.raises(ValueError, match="[Cc]ircular"): + Net(config) + +# %% +test_depends_on_circular_raises() + +# %% +#|export +def test_depends_on_invalid_node_raises(): + """Test that depends_on referencing non-existent node raises ValueError.""" + def noop(ctx, packets): + pass + + graph_config = GraphConfig( + nodes=[ + NodeConfig( + name="A", + in_ports={"in": PortConfig()}, + in_salvo_conditions={ + "default": SalvoConditionConfig( + max_salvos=MaxSalvosFiniteConfig(max=1), + ports={"in": PacketCountAllConfig()}, + term=SalvoConditionTermPortConfig( + port_name="in", + state=PortStateNonEmptyConfig(), + ), + ), + }, + execution_config=NodeExecutionConfig( + node_name="A", + pools=["main"], + exec_node_func=noop, + depends_on=["NonExistentNode"], + ), + ), + ], + edges=[], + ) + config = NetConfig( + pools={"main": PoolConfig(spec=MainPoolConfig())}, + graph=graph_config, + ) + + with pytest.raises(ValueError, match="NonExistentNode"): + Net(config) + +# %% +test_depends_on_invalid_node_raises() diff --git a/netrun/src/netrun/net/_net/_net.py b/netrun/src/netrun/net/_net/_net.py index 1b5ef7f0..b6019a3b 100644 --- a/netrun/src/netrun/net/_net/_net.py +++ b/netrun/src/netrun/net/_net/_net.py @@ -444,6 +444,36 @@ def __init__(self, config: NetConfig, run_init_nodes: bool = True): if node_config.execution_config is not None: self._node_execution_configs[node_config.name] = node_config.execution_config + # Validate depends_on: check for cycles and invalid references + all_node_names = {nc.name for nc in self._config_resolved.graph.nodes} + for node_name, exec_config in self._node_execution_configs.items(): + if exec_config.depends_on: + for dep in exec_config.depends_on: + if dep not in all_node_names: + raise ValueError( + f"Node '{node_name}' depends_on '{dep}' which does not exist in the graph" + ) + # Topological cycle check for depends_on + visited: set[str] = set() + path: set[str] = set() + def _check_cycle(node: str) -> None: + if node in path: + raise ValueError( + f"Circular dependency detected involving node '{node}'. " + f"depends_on must form a DAG (no cycles)." + ) + if node in visited: + return + path.add(node) + config = self._node_execution_configs.get(node) + if config and config.depends_on: + for dep in config.depends_on: + _check_cycle(dep) + path.remove(node) + visited.add(node) + for node_name in self._node_execution_configs: + _check_cycle(node_name) + # Disabled nodes set (populated from config, mutated at runtime) self._disabled_nodes: set[str] = set() for node_name, exec_config in self._node_execution_configs.items(): diff --git a/netrun/src/netrun/net/config/_nodes.py b/netrun/src/netrun/net/config/_nodes.py index 1e008bc7..ae5db37c 100644 --- a/netrun/src/netrun/net/config/_nodes.py +++ b/netrun/src/netrun/net/config/_nodes.py @@ -232,9 +232,9 @@ class NodeExecutionConfig(VarResolvableModel): rate_limit_per_second: float | VarRef | None = Field(default=None, description="Maximum epoch triggers per second.") - depends_on: list[str] | None = Field(default=None, description="Node names that must have completed at least one epoch before this node can start. Provides directional ordering without explicit control edges.") + depends_on: list[str] | None = Field(default=None, description="Node names that must have completed at least one epoch before this node can start. Provides directional ordering without explicit control edges. Must form a DAG — circular dependencies raise ValueError at Net construction. Non-existent node names also raise ValueError.") - resources: dict[str, int] | None = Field(default=None, description="Resource requirements as {resource_name: slots_needed}. Resources must be defined in NetConfig.resources with their capacity. Node epoch starts only when all required resource slots are available.") + resources: dict[str, int] | None = Field(default=None, description="Resource requirements as {resource_name: slots_needed}. Resources must be defined in NetConfig.resources with their capacity. Node epoch starts only when all required resource slots are available. Slots are acquired when epoch starts and released when epoch finishes, fails, or is cancelled.") retries: int | VarRef | None = Field(default=None, description="Number of retry attempts on failure. None inherits from NetConfig.") retry_wait: float | VarRef | None = Field(default=None, description="Wait time in seconds between retries. None inherits from NetConfig.") diff --git a/netrun/src/tests/net/test_net.py b/netrun/src/tests/net/test_net.py index c6a15e8a..4ffb96e6 100644 --- a/netrun/src/tests/net/test_net.py +++ b/netrun/src/tests/net/test_net.py @@ -1,6 +1,6 @@ # AUTOGENERATED! DO NOT EDIT! File to edit: pts/tests/06_net/test_net.pct.py -__all__ = ['create_simple_graph_config', 'create_simple_net_config', 'test_async_epoch_callback', 'test_async_error_on_thread_pool', 'test_async_exec_func_on_main_pool', 'test_async_exec_func_on_thread_pool', 'test_async_from_function_factory_on_thread_pool', 'test_async_retry_on_thread_pool', 'test_async_subprocess_on_thread_pool', 'test_check_type_distinguishes_modes', 'test_check_type_string_generic_fails', 'test_check_type_string_simple', 'test_check_type_type_object_generic', 'test_check_type_type_object_generic_mismatch', 'test_check_type_type_object_simple', 'test_close_node_func_called_on_net_stop', 'test_concurrent_async_siblings_on_main_pool', 'test_context_cancel_epoch', 'test_context_consume_packet', 'test_context_consume_packet_not_found', 'test_context_create_packet', 'test_context_create_packet_from_value_func', 'test_context_full_workflow', 'test_context_get_execution_result', 'test_context_load_output_port', 'test_context_print_accumulates', 'test_context_print_basic', 'test_context_print_custom_separators', 'test_context_print_echo_stdout', 'test_context_print_empty', 'test_context_print_flush_ignored', 'test_context_print_multiple_timestamps', 'test_context_print_non_string', 'test_context_send_output_salvo', 'test_create_func_preprocessor_from_config', 'test_create_func_preprocessor_from_config_empty', 'test_create_net_func_preprocessor_basic', 'test_create_net_func_preprocessor_captures_exception', 'test_create_net_func_preprocessor_captures_prints', 'test_create_net_func_preprocessor_with_retry_info', 'test_ctx_state_cleared_on_success', 'test_ctx_state_empty_by_default', 'test_ctx_state_persists_across_retries', 'test_ctx_vars_access', 'test_ctx_vars_empty', 'test_ctx_vars_inherit_missing_global_raises', 'test_ctx_vars_merging', 'test_ctx_vars_merging_with_inherit', 'test_dead_letter_queue_after_max_retries', 'test_dead_letter_queue_has_pool_and_worker', 'test_defer_init_delays_init_node_func', 'test_deferred_actions_preserved_in_result', 'test_deferred_queue_add_consume_packet', 'test_deferred_queue_add_create_packet', 'test_deferred_queue_add_load_output_port', 'test_deferred_queue_add_send_output_salvo', 'test_deferred_queue_discard', 'test_deferred_queue_multiple_creates', 'test_depends_on_blocks_until_dependency_completes', 'test_depends_on_multiple_dependencies', 'test_depends_on_none_is_noop', 'test_enable_disable_unknown_node', 'test_epoch_callback_deregistration', 'test_epoch_cancelled_can_be_raised_and_caught', 'test_epoch_cancelled_exception', 'test_epoch_end_on_cancelled', 'test_epoch_error_basic', 'test_epoch_error_can_be_caught', 'test_epoch_error_chaining', 'test_epoch_error_minimal', 'test_epoch_error_str', 'test_epoch_execution_simple_node', 'test_epoch_execution_with_output', 'test_epoch_record_cancellation_lifecycle', 'test_epoch_record_lifecycle_timestamps', 'test_epoch_record_out_salvos_populated', 'test_exception_queue_contains_epoch_error', 'test_execute_node_calls_init_node_func', 'test_execute_node_inside_net_basic', 'test_execute_node_inside_net_source_node', 'test_execute_node_outside_net', 'test_execute_node_outside_net_async_func', 'test_execute_node_salvo_condition_not_satisfied', 'test_execution_manager_get_pool', 'test_factory_node_exec_func_override', 'test_faulthandler_enabled_after_init', 'test_global_max_epochs_default_unlimited', 'test_global_max_epochs_limits_all_nodes', 'test_init_node_func_called_on_net_start', 'test_init_node_func_called_once_with_defer_init', 'test_is_blocked_with_disabled_node', 'test_max_epochs_allows_up_to_limit', 'test_max_epochs_exceeded_exception', 'test_max_epochs_none_unlimited', 'test_max_epochs_one_raises_on_second', 'test_max_epochs_queued_when_not_propagating', 'test_max_parallel_epochs_limits_concurrent_starts', 'test_max_parallel_epochs_none_allows_all', 'test_max_parallel_epochs_per_node', 'test_multiple_epoch_callbacks', 'test_net_check_rate_limit_enforced', 'test_net_check_rate_limit_no_config', 'test_net_check_rate_limit_none_limit', 'test_net_check_rate_limit_window_expires', 'test_net_clear_dead_letter_queue', 'test_net_close_sync', 'test_net_config_default_pool_allocation_method', 'test_net_config_default_pool_allocation_method_default', 'test_net_config_property', 'test_net_config_type_checking', 'test_net_context_manager', 'test_net_creation', 'test_net_dead_letter_queue_empty', 'test_net_dead_letter_queue_returns_copy', 'test_net_from_file_json', 'test_net_from_file_not_found', 'test_net_from_file_unsupported_format', 'test_net_get_epoch_log_empty', 'test_net_get_node_logs_empty', 'test_net_get_startable_epochs', 'test_net_graph_property', 'test_net_handle_print_buffer', 'test_net_init_sync', 'test_net_install_sigint_handler', 'test_net_invalid_pool_type_raises', 'test_net_is_blocked_empty_network', 'test_net_is_blocked_with_running_epochs', 'test_net_pause_and_resume', 'test_net_protocol_keys_uniqueness', 'test_net_protocol_keys_values', 'test_net_run_step', 'test_net_run_until_blocked', 'test_net_running_epochs_empty', 'test_net_start_and_stop', 'test_net_start_background', 'test_net_start_background_already_running', 'test_net_start_twice_raises', 'test_net_with_multiple_pool_types', 'test_net_with_node_execution_configs', 'test_node_disable_at_runtime', 'test_node_disabled_via_config', 'test_node_enable_at_runtime', 'test_node_execution_config_defaults', 'test_node_execution_config_max_epochs_default', 'test_node_execution_config_max_epochs_set', 'test_node_execution_config_new_fields', 'test_node_execution_config_retry_defaults', 'test_node_execution_config_type_checking', 'test_node_execution_config_with_retries', 'test_node_execution_context_creation', 'test_node_execution_context_with_retry_info', 'test_node_execution_result_with_exception', 'test_node_execution_result_with_func_result', 'test_node_failure_context_creation', 'test_node_failure_context_full', 'test_node_failure_context_has_pool_and_worker', 'test_node_failure_context_print', 'test_node_level_override_print', 'test_node_level_override_propagate', 'test_node_scoped_epoch_callbacks', 'test_on_epoch_end_callback', 'test_on_epoch_start_callback', 'test_on_node_failure_callback', 'test_per_node_max_epochs_overrides_global', 'test_per_node_minus_one_overrides_global_limit', 'test_pool_server_context_as_context_manager_still_works', 'test_pool_server_context_log_no_file', 'test_pool_server_context_start_stop', 'test_pool_server_context_start_stop_with_log_file', 'test_preprocessor_handles_cancel_epoch', 'test_preprocessor_type_checking_inheritance', 'test_print_exceptions_includes_pool_worker', 'test_print_exceptions_true_prints_to_stderr', 'test_propagate_exceptions_false_queues', 'test_propagate_exceptions_true_raises', 'test_propagate_exceptions_wraps_in_epoch_error', 'test_request_pool_shutdown_end_to_end', 'test_request_pool_shutdown_non_remote_raises', 'test_resources_multi_slot', 'test_resources_mutual_exclusion', 'test_resources_none_is_noop', 'test_resources_undefined_raises', 'test_retry_calls_gc_collect', 'test_retry_on_failure', 'test_run_on_init_basic', 'test_run_on_init_disabled', 'test_run_on_init_error_no_valid_condition', 'test_run_on_init_outputs_flow_downstream', 'test_run_on_init_skip_via_constructor', 'test_run_on_init_skip_via_start_param', 'test_run_on_init_start_param_overrides_constructor', 'test_serve_pool_accepts_path', 'test_serve_pool_accepts_pathlib_path', 'test_serve_pool_custom_worker_name', 'test_serve_pool_end_to_end_with_net', 'test_serve_pool_log_file_writes', 'test_serve_pool_no_log_file', 'test_serve_pool_returns_pool_server_context', 'test_serve_pool_starts_and_stops_server', 'test_serve_pool_with_log_file', 'test_server_log_callback', 'test_start_stop_with_async_funcs', 'test_stop_not_called_for_unstarted_deferred_node', 'test_streaming_downstream_starts_before_slow_sibling', 'test_streaming_error_propagates', 'test_streaming_linear_pipeline', 'test_streaming_max_parallel_epochs_across_rounds', 'test_timeout_enforcement_raises_epoch_error', 'test_timeout_goes_to_dead_letter_queue', 'test_timeout_none_no_limit', 'test_type_checking_disabled'] +__all__ = ['create_simple_graph_config', 'create_simple_net_config', 'test_async_epoch_callback', 'test_async_error_on_thread_pool', 'test_async_exec_func_on_main_pool', 'test_async_exec_func_on_thread_pool', 'test_async_from_function_factory_on_thread_pool', 'test_async_retry_on_thread_pool', 'test_async_subprocess_on_thread_pool', 'test_check_type_distinguishes_modes', 'test_check_type_string_generic_fails', 'test_check_type_string_simple', 'test_check_type_type_object_generic', 'test_check_type_type_object_generic_mismatch', 'test_check_type_type_object_simple', 'test_close_node_func_called_on_net_stop', 'test_concurrent_async_siblings_on_main_pool', 'test_context_cancel_epoch', 'test_context_consume_packet', 'test_context_consume_packet_not_found', 'test_context_create_packet', 'test_context_create_packet_from_value_func', 'test_context_full_workflow', 'test_context_get_execution_result', 'test_context_load_output_port', 'test_context_print_accumulates', 'test_context_print_basic', 'test_context_print_custom_separators', 'test_context_print_echo_stdout', 'test_context_print_empty', 'test_context_print_flush_ignored', 'test_context_print_multiple_timestamps', 'test_context_print_non_string', 'test_context_send_output_salvo', 'test_create_func_preprocessor_from_config', 'test_create_func_preprocessor_from_config_empty', 'test_create_net_func_preprocessor_basic', 'test_create_net_func_preprocessor_captures_exception', 'test_create_net_func_preprocessor_captures_prints', 'test_create_net_func_preprocessor_with_retry_info', 'test_ctx_state_cleared_on_success', 'test_ctx_state_empty_by_default', 'test_ctx_state_persists_across_retries', 'test_ctx_vars_access', 'test_ctx_vars_empty', 'test_ctx_vars_inherit_missing_global_raises', 'test_ctx_vars_merging', 'test_ctx_vars_merging_with_inherit', 'test_dead_letter_queue_after_max_retries', 'test_dead_letter_queue_has_pool_and_worker', 'test_defer_init_delays_init_node_func', 'test_deferred_actions_preserved_in_result', 'test_deferred_queue_add_consume_packet', 'test_deferred_queue_add_create_packet', 'test_deferred_queue_add_load_output_port', 'test_deferred_queue_add_send_output_salvo', 'test_deferred_queue_discard', 'test_deferred_queue_multiple_creates', 'test_depends_on_blocks_until_dependency_completes', 'test_depends_on_circular_raises', 'test_depends_on_invalid_node_raises', 'test_depends_on_multiple_dependencies', 'test_depends_on_none_is_noop', 'test_enable_disable_unknown_node', 'test_epoch_callback_deregistration', 'test_epoch_cancelled_can_be_raised_and_caught', 'test_epoch_cancelled_exception', 'test_epoch_end_on_cancelled', 'test_epoch_error_basic', 'test_epoch_error_can_be_caught', 'test_epoch_error_chaining', 'test_epoch_error_minimal', 'test_epoch_error_str', 'test_epoch_execution_simple_node', 'test_epoch_execution_with_output', 'test_epoch_record_cancellation_lifecycle', 'test_epoch_record_lifecycle_timestamps', 'test_epoch_record_out_salvos_populated', 'test_exception_queue_contains_epoch_error', 'test_execute_node_calls_init_node_func', 'test_execute_node_inside_net_basic', 'test_execute_node_inside_net_source_node', 'test_execute_node_outside_net', 'test_execute_node_outside_net_async_func', 'test_execute_node_salvo_condition_not_satisfied', 'test_execution_manager_get_pool', 'test_factory_node_exec_func_override', 'test_faulthandler_enabled_after_init', 'test_global_max_epochs_default_unlimited', 'test_global_max_epochs_limits_all_nodes', 'test_init_node_func_called_on_net_start', 'test_init_node_func_called_once_with_defer_init', 'test_is_blocked_with_disabled_node', 'test_max_epochs_allows_up_to_limit', 'test_max_epochs_exceeded_exception', 'test_max_epochs_none_unlimited', 'test_max_epochs_one_raises_on_second', 'test_max_epochs_queued_when_not_propagating', 'test_max_parallel_epochs_limits_concurrent_starts', 'test_max_parallel_epochs_none_allows_all', 'test_max_parallel_epochs_per_node', 'test_multiple_epoch_callbacks', 'test_net_check_rate_limit_enforced', 'test_net_check_rate_limit_no_config', 'test_net_check_rate_limit_none_limit', 'test_net_check_rate_limit_window_expires', 'test_net_clear_dead_letter_queue', 'test_net_close_sync', 'test_net_config_default_pool_allocation_method', 'test_net_config_default_pool_allocation_method_default', 'test_net_config_property', 'test_net_config_type_checking', 'test_net_context_manager', 'test_net_creation', 'test_net_dead_letter_queue_empty', 'test_net_dead_letter_queue_returns_copy', 'test_net_from_file_json', 'test_net_from_file_not_found', 'test_net_from_file_unsupported_format', 'test_net_get_epoch_log_empty', 'test_net_get_node_logs_empty', 'test_net_get_startable_epochs', 'test_net_graph_property', 'test_net_handle_print_buffer', 'test_net_init_sync', 'test_net_install_sigint_handler', 'test_net_invalid_pool_type_raises', 'test_net_is_blocked_empty_network', 'test_net_is_blocked_with_running_epochs', 'test_net_pause_and_resume', 'test_net_protocol_keys_uniqueness', 'test_net_protocol_keys_values', 'test_net_run_step', 'test_net_run_until_blocked', 'test_net_running_epochs_empty', 'test_net_start_and_stop', 'test_net_start_background', 'test_net_start_background_already_running', 'test_net_start_twice_raises', 'test_net_with_multiple_pool_types', 'test_net_with_node_execution_configs', 'test_node_disable_at_runtime', 'test_node_disabled_via_config', 'test_node_enable_at_runtime', 'test_node_execution_config_defaults', 'test_node_execution_config_max_epochs_default', 'test_node_execution_config_max_epochs_set', 'test_node_execution_config_new_fields', 'test_node_execution_config_retry_defaults', 'test_node_execution_config_type_checking', 'test_node_execution_config_with_retries', 'test_node_execution_context_creation', 'test_node_execution_context_with_retry_info', 'test_node_execution_result_with_exception', 'test_node_execution_result_with_func_result', 'test_node_failure_context_creation', 'test_node_failure_context_full', 'test_node_failure_context_has_pool_and_worker', 'test_node_failure_context_print', 'test_node_level_override_print', 'test_node_level_override_propagate', 'test_node_scoped_epoch_callbacks', 'test_on_epoch_end_callback', 'test_on_epoch_start_callback', 'test_on_node_failure_callback', 'test_per_node_max_epochs_overrides_global', 'test_per_node_minus_one_overrides_global_limit', 'test_pool_server_context_as_context_manager_still_works', 'test_pool_server_context_log_no_file', 'test_pool_server_context_start_stop', 'test_pool_server_context_start_stop_with_log_file', 'test_preprocessor_handles_cancel_epoch', 'test_preprocessor_type_checking_inheritance', 'test_print_exceptions_includes_pool_worker', 'test_print_exceptions_true_prints_to_stderr', 'test_propagate_exceptions_false_queues', 'test_propagate_exceptions_true_raises', 'test_propagate_exceptions_wraps_in_epoch_error', 'test_request_pool_shutdown_end_to_end', 'test_request_pool_shutdown_non_remote_raises', 'test_resources_multi_slot', 'test_resources_mutual_exclusion', 'test_resources_none_is_noop', 'test_resources_undefined_raises', 'test_retry_calls_gc_collect', 'test_retry_on_failure', 'test_run_on_init_basic', 'test_run_on_init_disabled', 'test_run_on_init_error_no_valid_condition', 'test_run_on_init_outputs_flow_downstream', 'test_run_on_init_skip_via_constructor', 'test_run_on_init_skip_via_start_param', 'test_run_on_init_start_param_overrides_constructor', 'test_serve_pool_accepts_path', 'test_serve_pool_accepts_pathlib_path', 'test_serve_pool_custom_worker_name', 'test_serve_pool_end_to_end_with_net', 'test_serve_pool_log_file_writes', 'test_serve_pool_no_log_file', 'test_serve_pool_returns_pool_server_context', 'test_serve_pool_starts_and_stops_server', 'test_serve_pool_with_log_file', 'test_server_log_callback', 'test_start_stop_with_async_funcs', 'test_stop_not_called_for_unstarted_deferred_node', 'test_streaming_downstream_starts_before_slow_sibling', 'test_streaming_error_propagates', 'test_streaming_linear_pipeline', 'test_streaming_max_parallel_epochs_across_rounds', 'test_timeout_enforcement_raises_epoch_error', 'test_timeout_goes_to_dead_letter_queue', 'test_timeout_none_no_limit', 'test_type_checking_disabled'] # %% pts/tests/06_net/test_net.pct.py 2 import pytest @@ -7072,3 +7072,85 @@ def simple_node(ctx, packets): net.inject_data("BadNode", "in", [1]) with pytest.raises(ValueError, match="nonexistent_resource"): await net.run_until_blocked() + +# %% pts/tests/06_net/test_net.pct.py 384 +def test_depends_on_circular_raises(): + """Test that circular depends_on raises ValueError at construction.""" + def noop(ctx, packets): + pass + + def make_node(name, depends_on=None): + return NodeConfig( + name=name, + in_ports={"in": PortConfig()}, + in_salvo_conditions={ + "default": SalvoConditionConfig( + max_salvos=MaxSalvosFiniteConfig(max=1), + ports={"in": PacketCountAllConfig()}, + term=SalvoConditionTermPortConfig( + port_name="in", + state=PortStateNonEmptyConfig(), + ), + ), + }, + execution_config=NodeExecutionConfig( + node_name=name, + pools=["main"], + exec_node_func=noop, + depends_on=depends_on, + ), + ) + + graph_config = GraphConfig( + nodes=[ + make_node("A", depends_on=["B"]), + make_node("B", depends_on=["A"]), + ], + edges=[], + ) + config = NetConfig( + pools={"main": PoolConfig(spec=MainPoolConfig())}, + graph=graph_config, + ) + + with pytest.raises(ValueError, match="[Cc]ircular"): + Net(config) + +# %% pts/tests/06_net/test_net.pct.py 386 +def test_depends_on_invalid_node_raises(): + """Test that depends_on referencing non-existent node raises ValueError.""" + def noop(ctx, packets): + pass + + graph_config = GraphConfig( + nodes=[ + NodeConfig( + name="A", + in_ports={"in": PortConfig()}, + in_salvo_conditions={ + "default": SalvoConditionConfig( + max_salvos=MaxSalvosFiniteConfig(max=1), + ports={"in": PacketCountAllConfig()}, + term=SalvoConditionTermPortConfig( + port_name="in", + state=PortStateNonEmptyConfig(), + ), + ), + }, + execution_config=NodeExecutionConfig( + node_name="A", + pools=["main"], + exec_node_func=noop, + depends_on=["NonExistentNode"], + ), + ), + ], + edges=[], + ) + config = NetConfig( + pools={"main": PoolConfig(spec=MainPoolConfig())}, + graph=graph_config, + ) + + with pytest.raises(ValueError, match="NonExistentNode"): + Net(config) From f6a3ed029dbf748839b5f0ef366eb68b4d22b8c2 Mon Sep 17 00:00:00 2001 From: Lukas Kikuchi Date: Mon, 27 Apr 2026 16:40:49 +0100 Subject: [PATCH 08/15] fix(netrun): isolate worker crashes so they don't kill the entire pool MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, when a worker crashed, BasePool.recv() raised WorkerCrashed which killed _msg_recv_task_func — the central message receiver for the entire pool. This made ALL workers in the pool unreachable, not just the crashed one. Now: - BasePool.recv_raw() returns error messages without raising, for use by ExecutionManager's message receiver - _msg_recv_task_func uses recv_raw() and routes pool-level errors to the specific jobs running on the affected worker - _msg_worker_map tracks msg_id -> worker_id for error routing - _recv_msg detects routed error messages and raises to the caller - Other workers remain fully functional after a single worker crash BasePool.recv() is unchanged — external callers still get exceptions. Co-Authored-By: Claude Opus 4.6 (1M context) --- netrun/pts/netrun/04_pool/00_base.pct.py | 25 ++++++++ netrun/pts/netrun/04_pool/03_remote.pct.py | 34 +++++++++++ netrun/pts/netrun/05_execution_manager.pct.py | 30 +++++++++- .../test_execution_manager_thread.pct.py | 60 +++++++++++++++++++ netrun/src/netrun/execution_manager.py | 30 +++++++++- netrun/src/netrun/pool/base.py | 25 ++++++++ netrun/src/netrun/pool/remote.py | 34 +++++++++++ .../test_execution_manager_thread.py | 55 ++++++++++++++++- 8 files changed, 286 insertions(+), 7 deletions(-) diff --git a/netrun/pts/netrun/04_pool/00_base.pct.py b/netrun/pts/netrun/04_pool/00_base.pct.py index 91fc2b77..db168cc9 100644 --- a/netrun/pts/netrun/04_pool/00_base.pct.py +++ b/netrun/pts/netrun/04_pool/00_base.pct.py @@ -389,6 +389,31 @@ async def recv(self, timeout: float | None = None) -> WorkerMessage: _check_error_and_raise(msg) return msg + async def recv_raw(self, timeout: float | None = None) -> WorkerMessage: + """Like recv() but returns error messages instead of raising. + + Used by ExecutionManager to handle worker errors gracefully + without killing the pool's central message receiver. Error messages + (WorkerCrashed, WorkerException, etc.) are returned as normal + WorkerMessage objects for the caller to route appropriately. + """ + if not self._running: + raise PoolNotStarted("Pool has not been started") + self._start_recv_tasks() + try: + if timeout is None: + msg = await self._recv_queue.get() + else: + msg = await asyncio.wait_for( + self._recv_queue.get(), + timeout=timeout, + ) + except (TimeoutError, asyncio.TimeoutError): + from netrun.rpc.base import RecvTimeout + raise RecvTimeout(f"Receive timed out after {timeout}s") + self._last_message_time[msg.worker_id] = time.monotonic() + return msg + async def try_recv(self) -> WorkerMessage | None: """Non-blocking receive from any worker. diff --git a/netrun/pts/netrun/04_pool/03_remote.pct.py b/netrun/pts/netrun/04_pool/03_remote.pct.py index 16c3e374..2fe113a0 100644 --- a/netrun/pts/netrun/04_pool/03_remote.pct.py +++ b/netrun/pts/netrun/04_pool/03_remote.pct.py @@ -88,6 +88,8 @@ PoolNotStarted, WorkerException, WorkerCrashed, + POOL_UP_ERROR_EXCEPTION, + POOL_UP_ERROR_CRASHED, ) from netrun.pool.multiprocess import MultiprocessPool, OutputBuffer @@ -666,6 +668,38 @@ async def recv(self, timeout: float | None = None) -> WorkerMessage: data=data["data"], ) + async def recv_raw(self, timeout: float | None = None) -> WorkerMessage: + """Like recv() but returns error messages instead of raising. + + Error messages are returned as WorkerMessage with POOL_UP_ERROR_* keys. + """ + if not self._running: + raise PoolNotStarted("Pool not created") + + try: + if timeout is None: + item = await self._recv_queue.get() + else: + item = await asyncio.wait_for(self._recv_queue.get(), timeout=timeout) + except TimeoutError: + raise RecvTimeout(f"Receive timed out after {timeout}s") + + msg_type, data = item + if msg_type == "error": + worker_id = data.get("worker_id", -1) + if data.get("type") == "WorkerCrashed": + return WorkerMessage(worker_id=worker_id, key=POOL_UP_ERROR_CRASHED, data=data) + else: + return WorkerMessage(worker_id=worker_id, key=POOL_UP_ERROR_EXCEPTION, data=data) + elif msg_type == "pool_error": + return WorkerMessage(worker_id=-1, key=POOL_UP_ERROR_CRASHED, data={"reason": f"Server error: {data}"}) + + return WorkerMessage( + worker_id=data["worker_id"], + key=data["key"], + data=data["data"], + ) + async def try_recv(self) -> WorkerMessage | None: """Non-blocking receive from any worker. diff --git a/netrun/pts/netrun/05_execution_manager.pct.py b/netrun/pts/netrun/05_execution_manager.pct.py index 3bddac3e..5aa88016 100644 --- a/netrun/pts/netrun/05_execution_manager.pct.py +++ b/netrun/pts/netrun/05_execution_manager.pct.py @@ -37,6 +37,7 @@ import functools from netrun.rpc.base import RPCChannel +from netrun.pool.base import POOL_UP_ERROR_KEYS, _check_error_and_raise from netrun.pool.thread import ThreadPool from netrun.pool.multiprocess import MultiprocessPool from netrun.pool.aio import SingleWorkerPool @@ -493,6 +494,7 @@ def __init__(self, pool_configs: dict[str, tuple[type[PoolType], dict[str, Any]] self._worker_jobs: dict[tuple[str, str], list[SubmittedJobInfo]] = {} # (pool_id, worker_id) -> list of SubmittedJobInfo self._round_robin_counter: int = 0 + self._msg_worker_map: dict[str, dict[str, int]] = {} # pool_id -> {msg_id -> worker_id} # Shared event loop for async function dispatch in thread/multiprocess workers. # Created in start(), cleaned up in close(). @@ -562,11 +564,25 @@ async def start(self) -> None: async def _msg_recv_task_func(self, pool_id: str): pool = self._pools[pool_id] while True: - msg = await pool.recv() + msg = await pool.recv_raw() + + # Pool-level error (worker crashed/died) — route to affected jobs + # instead of crashing the entire receiver task. + if msg.key in POOL_UP_ERROR_KEYS: + worker_id = msg.worker_id + affected_msg_ids = [ + mid for mid, wid in self._msg_worker_map.get(pool_id, {}).items() + if wid == worker_id + ] + for mid in affected_msg_ids: + queue = self._msgs[pool_id].get(mid) + if queue is not None: + await queue.put(msg) + continue + + # Normal message — route by msg_id msg_id = msg.data[0] msg.data = msg.data[1:] - # Queue may have been cleaned up if the job was cancelled/timed out. - # Silently discard orphaned responses. queue = self._msgs[pool_id].get(msg_id) if queue is not None: await queue.put(msg) @@ -586,6 +602,8 @@ async def _send_msg(self, pool_id: str, worker_id: str, key: str, data: Any) -> # Create the queue BEFORE sending to avoid race condition where response # arrives before the queue is created self._msgs[pool_id][msg_id] = asyncio.Queue() + # Track which worker this msg_id is for (needed to route worker errors) + self._msg_worker_map.setdefault(pool_id, {})[msg_id] = worker_id await pool.send( worker_id=worker_id, @@ -632,6 +650,11 @@ async def _recv_msg(self, pool_id: str, msg_id: str, expect: ExecutionManagerPro if close_msg_queue: del self._msgs[pool_id][msg_id] + self._msg_worker_map.get(pool_id, {}).pop(msg_id, None) + + # Check for pool-level error routed from _msg_recv_task_func + if msg.key in POOL_UP_ERROR_KEYS: + _check_error_and_raise(msg) if msg.key != expect.value: raise ValueError(f"Expected message key '{expect.value}', got '{msg.key}'.") @@ -718,6 +741,7 @@ async def run( pass # Already removed on the success path if msg_id is not None and msg_id in self._msgs.get(pool_id, {}): del self._msgs[pool_id][msg_id] + self._msg_worker_map.get(pool_id, {}).pop(msg_id, None) async def run_allocate( self, diff --git a/netrun/pts/tests/05_execution_manager/test_execution_manager_thread.pct.py b/netrun/pts/tests/05_execution_manager/test_execution_manager_thread.pct.py index ce0b37a1..968de41e 100644 --- a/netrun/pts/tests/05_execution_manager/test_execution_manager_thread.pct.py +++ b/netrun/pts/tests/05_execution_manager/test_execution_manager_thread.pct.py @@ -945,3 +945,63 @@ async def test_shared_loop_cleanup(): # %% await test_shared_loop_cleanup(); + +# %% [markdown] +# ## Worker crash isolation tests + +# %% +#|export +@pytest.mark.asyncio +async def test_worker_crash_does_not_kill_other_workers(): + """Test that a worker crash doesn't prevent other workers from functioning. + + Previously, a worker crash killed the _msg_recv_task_func for the entire + pool, making all workers unreachable. Now, the error is routed only to + the affected job and the receiver continues. + """ + import os + import signal as _signal + + def crash_worker_func(a: int, b: int) -> int: + """Function that crashes the worker by raising from a non-preprocessor context.""" + raise RuntimeError("Worker crash!") + + manager = ExecutionManager({ + "pool": (ThreadPool, {"num_workers": 2}), + }) + async with manager: + await manager.send_function("pool", 0, "crash", crash_worker_func) + await manager.send_function("pool", 1, "crash", crash_worker_func) + await manager.send_function("pool", 0, "add", add_numbers) + await manager.send_function("pool", 1, "add", add_numbers) + + # Worker 0 crashes + with pytest.raises(Exception): + await asyncio.wait_for( + manager.run( + pool_id="pool", + worker_id=0, + func_import_path_or_key="crash", + send_channel=False, + func_args=(1, 2), + func_kwargs={}, + ), + timeout=5.0, + ) + + # Worker 1 should still be functional + result = await asyncio.wait_for( + manager.run( + pool_id="pool", + worker_id=1, + func_import_path_or_key="add", + send_channel=False, + func_args=(10, 20), + func_kwargs={}, + ), + timeout=5.0, + ) + assert result.result == 30 + +# %% +await test_worker_crash_does_not_kill_other_workers(); diff --git a/netrun/src/netrun/execution_manager.py b/netrun/src/netrun/execution_manager.py index 3bdf29be..d2ef6fc5 100644 --- a/netrun/src/netrun/execution_manager.py +++ b/netrun/src/netrun/execution_manager.py @@ -18,6 +18,7 @@ import functools from .rpc.base import RPCChannel +from .pool.base import POOL_UP_ERROR_KEYS, _check_error_and_raise from .pool.thread import ThreadPool from .pool.multiprocess import MultiprocessPool from .pool.aio import SingleWorkerPool @@ -442,6 +443,7 @@ def __init__(self, pool_configs: dict[str, tuple[type[PoolType], dict[str, Any]] self._worker_jobs: dict[tuple[str, str], list[SubmittedJobInfo]] = {} # (pool_id, worker_id) -> list of SubmittedJobInfo self._round_robin_counter: int = 0 + self._msg_worker_map: dict[str, dict[str, int]] = {} # pool_id -> {msg_id -> worker_id} # Shared event loop for async function dispatch in thread/multiprocess workers. # Created in start(), cleaned up in close(). @@ -511,11 +513,25 @@ async def start(self) -> None: async def _msg_recv_task_func(self, pool_id: str): pool = self._pools[pool_id] while True: - msg = await pool.recv() + msg = await pool.recv_raw() + + # Pool-level error (worker crashed/died) — route to affected jobs + # instead of crashing the entire receiver task. + if msg.key in POOL_UP_ERROR_KEYS: + worker_id = msg.worker_id + affected_msg_ids = [ + mid for mid, wid in self._msg_worker_map.get(pool_id, {}).items() + if wid == worker_id + ] + for mid in affected_msg_ids: + queue = self._msgs[pool_id].get(mid) + if queue is not None: + await queue.put(msg) + continue + + # Normal message — route by msg_id msg_id = msg.data[0] msg.data = msg.data[1:] - # Queue may have been cleaned up if the job was cancelled/timed out. - # Silently discard orphaned responses. queue = self._msgs[pool_id].get(msg_id) if queue is not None: await queue.put(msg) @@ -535,6 +551,8 @@ async def _send_msg(self, pool_id: str, worker_id: str, key: str, data: Any) -> # Create the queue BEFORE sending to avoid race condition where response # arrives before the queue is created self._msgs[pool_id][msg_id] = asyncio.Queue() + # Track which worker this msg_id is for (needed to route worker errors) + self._msg_worker_map.setdefault(pool_id, {})[msg_id] = worker_id await pool.send( worker_id=worker_id, @@ -581,6 +599,11 @@ async def _recv_msg(self, pool_id: str, msg_id: str, expect: ExecutionManagerPro if close_msg_queue: del self._msgs[pool_id][msg_id] + self._msg_worker_map.get(pool_id, {}).pop(msg_id, None) + + # Check for pool-level error routed from _msg_recv_task_func + if msg.key in POOL_UP_ERROR_KEYS: + _check_error_and_raise(msg) if msg.key != expect.value: raise ValueError(f"Expected message key '{expect.value}', got '{msg.key}'.") @@ -667,6 +690,7 @@ async def run( pass # Already removed on the success path if msg_id is not None and msg_id in self._msgs.get(pool_id, {}): del self._msgs[pool_id][msg_id] + self._msg_worker_map.get(pool_id, {}).pop(msg_id, None) async def run_allocate( self, diff --git a/netrun/src/netrun/pool/base.py b/netrun/src/netrun/pool/base.py index fd462775..9f63dea9 100644 --- a/netrun/src/netrun/pool/base.py +++ b/netrun/src/netrun/pool/base.py @@ -326,6 +326,31 @@ async def recv(self, timeout: float | None = None) -> WorkerMessage: _check_error_and_raise(msg) return msg + async def recv_raw(self, timeout: float | None = None) -> WorkerMessage: + """Like recv() but returns error messages instead of raising. + + Used by ExecutionManager to handle worker errors gracefully + without killing the pool's central message receiver. Error messages + (WorkerCrashed, WorkerException, etc.) are returned as normal + WorkerMessage objects for the caller to route appropriately. + """ + if not self._running: + raise PoolNotStarted("Pool has not been started") + self._start_recv_tasks() + try: + if timeout is None: + msg = await self._recv_queue.get() + else: + msg = await asyncio.wait_for( + self._recv_queue.get(), + timeout=timeout, + ) + except (TimeoutError, asyncio.TimeoutError): + from ..rpc.base import RecvTimeout + raise RecvTimeout(f"Receive timed out after {timeout}s") + self._last_message_time[msg.worker_id] = time.monotonic() + return msg + async def try_recv(self) -> WorkerMessage | None: """Non-blocking receive from any worker. diff --git a/netrun/src/netrun/pool/remote.py b/netrun/src/netrun/pool/remote.py index a920de16..d6ffbdea 100644 --- a/netrun/src/netrun/pool/remote.py +++ b/netrun/src/netrun/pool/remote.py @@ -20,6 +20,8 @@ PoolNotStarted, WorkerException, WorkerCrashed, + POOL_UP_ERROR_EXCEPTION, + POOL_UP_ERROR_CRASHED, ) from ..pool.multiprocess import MultiprocessPool, OutputBuffer @@ -571,6 +573,38 @@ async def recv(self, timeout: float | None = None) -> WorkerMessage: data=data["data"], ) + async def recv_raw(self, timeout: float | None = None) -> WorkerMessage: + """Like recv() but returns error messages instead of raising. + + Error messages are returned as WorkerMessage with POOL_UP_ERROR_* keys. + """ + if not self._running: + raise PoolNotStarted("Pool not created") + + try: + if timeout is None: + item = await self._recv_queue.get() + else: + item = await asyncio.wait_for(self._recv_queue.get(), timeout=timeout) + except TimeoutError: + raise RecvTimeout(f"Receive timed out after {timeout}s") + + msg_type, data = item + if msg_type == "error": + worker_id = data.get("worker_id", -1) + if data.get("type") == "WorkerCrashed": + return WorkerMessage(worker_id=worker_id, key=POOL_UP_ERROR_CRASHED, data=data) + else: + return WorkerMessage(worker_id=worker_id, key=POOL_UP_ERROR_EXCEPTION, data=data) + elif msg_type == "pool_error": + return WorkerMessage(worker_id=-1, key=POOL_UP_ERROR_CRASHED, data={"reason": f"Server error: {data}"}) + + return WorkerMessage( + worker_id=data["worker_id"], + key=data["key"], + data=data["data"], + ) + async def try_recv(self) -> WorkerMessage | None: """Non-blocking receive from any worker. diff --git a/netrun/src/tests/execution_manager/test_execution_manager_thread.py b/netrun/src/tests/execution_manager/test_execution_manager_thread.py index 8c3d541e..f34e919d 100644 --- a/netrun/src/tests/execution_manager/test_execution_manager_thread.py +++ b/netrun/src/tests/execution_manager/test_execution_manager_thread.py @@ -1,6 +1,6 @@ # AUTOGENERATED! DO NOT EDIT! File to edit: pts/tests/05_execution_manager/test_execution_manager_thread.pct.py -__all__ = ['test_allocation_with_specific_workers', 'test_async_function', 'test_async_subprocess_in_thread_pool', 'test_asyncio_event_across_workers', 'test_concurrent_jobs', 'test_context_manager', 'test_create_execution_manager', 'test_create_multiple_pools', 'test_double_start_raises', 'test_empty_workers_raises', 'test_function_with_kwargs', 'test_get_num_workers', 'test_get_worker_jobs_empty', 'test_immediate_close', 'test_invalid_pool_type', 'test_job_result_timestamps', 'test_main_pool', 'test_multiple_pools', 'test_nested_async_in_thread_pool', 'test_non_serializable_result_for_main_process', 'test_pool_ids', 'test_random_allocation', 'test_round_robin_allocation', 'test_send_function_and_run', 'test_send_function_to_pool', 'test_shared_loop_cleanup', 'test_start_and_close', 'test_sync_function_unaffected_by_shared_loop', 'test_worker_exception_does_not_crash_loop', 'test_worker_jobs_cleanup_on_cancellation'] +__all__ = ['test_allocation_with_specific_workers', 'test_async_function', 'test_async_subprocess_in_thread_pool', 'test_asyncio_event_across_workers', 'test_concurrent_jobs', 'test_context_manager', 'test_create_execution_manager', 'test_create_multiple_pools', 'test_double_start_raises', 'test_empty_workers_raises', 'test_function_with_kwargs', 'test_get_num_workers', 'test_get_worker_jobs_empty', 'test_immediate_close', 'test_invalid_pool_type', 'test_job_result_timestamps', 'test_main_pool', 'test_multiple_pools', 'test_nested_async_in_thread_pool', 'test_non_serializable_result_for_main_process', 'test_pool_ids', 'test_random_allocation', 'test_round_robin_allocation', 'test_send_function_and_run', 'test_send_function_to_pool', 'test_shared_loop_cleanup', 'test_start_and_close', 'test_sync_function_unaffected_by_shared_loop', 'test_worker_crash_does_not_kill_other_workers', 'test_worker_exception_does_not_crash_loop', 'test_worker_jobs_cleanup_on_cancellation'] # %% pts/tests/05_execution_manager/test_execution_manager_thread.pct.py 2 import pytest @@ -769,3 +769,56 @@ async def test_shared_loop_cleanup(): # After close, the loop thread should have stopped assert not loop_thread.is_alive() assert manager._shared_loop is None + +# %% pts/tests/05_execution_manager/test_execution_manager_thread.pct.py 79 +@pytest.mark.asyncio +async def test_worker_crash_does_not_kill_other_workers(): + """Test that a worker crash doesn't prevent other workers from functioning. + + Previously, a worker crash killed the _msg_recv_task_func for the entire + pool, making all workers unreachable. Now, the error is routed only to + the affected job and the receiver continues. + """ + import os + import signal as _signal + + def crash_worker_func(a: int, b: int) -> int: + """Function that crashes the worker by raising from a non-preprocessor context.""" + raise RuntimeError("Worker crash!") + + manager = ExecutionManager({ + "pool": (ThreadPool, {"num_workers": 2}), + }) + async with manager: + await manager.send_function("pool", 0, "crash", crash_worker_func) + await manager.send_function("pool", 1, "crash", crash_worker_func) + await manager.send_function("pool", 0, "add", add_numbers) + await manager.send_function("pool", 1, "add", add_numbers) + + # Worker 0 crashes + with pytest.raises(Exception): + await asyncio.wait_for( + manager.run( + pool_id="pool", + worker_id=0, + func_import_path_or_key="crash", + send_channel=False, + func_args=(1, 2), + func_kwargs={}, + ), + timeout=5.0, + ) + + # Worker 1 should still be functional + result = await asyncio.wait_for( + manager.run( + pool_id="pool", + worker_id=1, + func_import_path_or_key="add", + send_channel=False, + func_args=(10, 20), + func_kwargs={}, + ), + timeout=5.0, + ) + assert result.result == 30 From 927c0bca484ded102f8ee8afcd27f2b13cbbd6cc Mon Sep 17 00:00:00 2001 From: Lukas Kikuchi Date: Mon, 27 Apr 2026 17:31:18 +0100 Subject: [PATCH 09/15] docs(sample): add web scraping pipeline showcasing all major features MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New sample project (14_web_scraping_pipeline) demonstrating every major netrun feature in a single, realistic pipeline: [load_config] → [validate] → [broadcast] → 3 async scrapers → [join] → [analyze] → [export] Features demonstrated: - Async node functions on thread pool (shared event loop) - ctx.state: caches computation across retries - depends_on: setup_output waits for load_config without data edge - resources: http_conn=2 limits concurrent scrapes to 2 of 3 - Broadcast factory: fans config to parallel scrapers - Join factory: collects results from all scrapers - Retries: scrape_stocks simulates intermittent 503 failures - Structured logging (ctx.log) with key=value fields - Node variables (ctx.vars) for runtime-configurable URLs - Output queues for result collection - Signals (epoch_finished/epoch_failed) on all nodes - Thread pool with 3 workers + main pool Co-Authored-By: Claude Opus 4.6 (1M context) --- .../14_web_scraping_pipeline/main.netrun.json | 146 ++++++++++++ .../14_web_scraping_pipeline/main.py | 98 ++++++++ .../14_web_scraping_pipeline/nodes.py | 223 ++++++++++++++++++ 3 files changed, 467 insertions(+) create mode 100644 sample_projects/14_web_scraping_pipeline/main.netrun.json create mode 100644 sample_projects/14_web_scraping_pipeline/main.py create mode 100644 sample_projects/14_web_scraping_pipeline/nodes.py diff --git a/sample_projects/14_web_scraping_pipeline/main.netrun.json b/sample_projects/14_web_scraping_pipeline/main.netrun.json new file mode 100644 index 00000000..ba3a0c1b --- /dev/null +++ b/sample_projects/14_web_scraping_pipeline/main.netrun.json @@ -0,0 +1,146 @@ +{ + "extra": { + "description": "Web scraping pipeline demonstrating all major netrun features: async on thread pools, ctx.state, depends_on, resources, broadcast/join factories, retries, structured logging, node variables, and output queues." + }, + + "pools": { + "main": {"spec": {"type": "main"}}, + "scrape": {"spec": {"type": "thread", "num_workers": 3}} + }, + + "resources": { + "http_conn": 2 + }, + + "node_vars": { + "news_url": {"type": "str", "value": "https://example.com/news"}, + "weather_url": {"type": "str", "value": "https://example.com/weather"}, + "stocks_url": {"type": "str", "value": "https://example.com/stocks"} + }, + + "retries": 0, + "print_echo_stdout": true, + "propagate_exceptions": true, + + "default_signals": ["epoch_finished", "epoch_failed"], + + "output_queues": { + "results": { + "ports": [["export_report", "out"]] + } + }, + + "graph": { + "nodes": [ + { + "name": "load_config", + "factory": "netrun.node_factories.from_function", + "factory_args": {"func": "nodes.load_config"}, + "execution_config": { + "pools": ["main"], + "run_on_init": true + } + }, + { + "name": "validate_config", + "factory": "netrun.node_factories.from_function", + "factory_args": {"func": "nodes.validate_config"}, + "execution_config": { + "pools": ["main"] + } + }, + { + "name": "broadcast_config", + "factory": "netrun.node_factories.broadcast", + "factory_args": {"num_outputs": 3}, + "execution_config": { + "pools": ["main"] + } + }, + { + "name": "scrape_news", + "factory": "netrun.node_factories.from_function", + "factory_args": {"func": "nodes.scrape_news"}, + "execution_config": { + "pools": ["scrape"], + "resources": {"http_conn": 1}, + "retries": 1, + "retry_wait": 0.1 + } + }, + { + "name": "scrape_weather", + "factory": "netrun.node_factories.from_function", + "factory_args": {"func": "nodes.scrape_weather"}, + "execution_config": { + "pools": ["scrape"], + "resources": {"http_conn": 1}, + "retries": 1, + "retry_wait": 0.1 + } + }, + { + "name": "scrape_stocks", + "factory": "netrun.node_factories.from_function", + "factory_args": {"func": "nodes.scrape_stocks"}, + "execution_config": { + "pools": ["scrape"], + "resources": {"http_conn": 1}, + "retries": 2, + "retry_wait": 0.2 + } + }, + { + "name": "join_results", + "factory": "netrun.node_factories.join", + "factory_args": {"ports": ["news", "weather", "stocks"]}, + "execution_config": { + "pools": ["main"] + } + }, + { + "name": "analyze", + "factory": "netrun.node_factories.from_function", + "factory_args": {"func": "nodes.analyze"}, + "execution_config": { + "pools": ["main"], + "retries": 2, + "retry_wait": 0.0 + } + }, + { + "name": "setup_output", + "factory": "netrun.node_factories.from_function", + "factory_args": {"func": "nodes.setup_output"}, + "execution_config": { + "pools": ["main"], + "depends_on": ["load_config"] + } + }, + { + "name": "export_report", + "factory": "netrun.node_factories.from_function", + "factory_args": {"func": "nodes.export_report"}, + "execution_config": { + "pools": ["main"], + "depends_on": ["setup_output"] + } + } + ], + "edges": [ + {"source_node": "load_config", "source_port": "out", "target_node": "validate_config", "target_port": "config"}, + {"source_node": "validate_config", "source_port": "out", "target_node": "broadcast_config", "target_port": "in_0"}, + + {"source_node": "broadcast_config", "source_port": "out_0", "target_node": "scrape_news", "target_port": "config"}, + {"source_node": "broadcast_config", "source_port": "out_1", "target_node": "scrape_weather", "target_port": "config"}, + {"source_node": "broadcast_config", "source_port": "out_2", "target_node": "scrape_stocks", "target_port": "config"}, + + {"source_node": "scrape_news", "source_port": "out", "target_node": "join_results", "target_port": "news"}, + {"source_node": "scrape_weather", "source_port": "out", "target_node": "join_results", "target_port": "weather"}, + {"source_node": "scrape_stocks", "source_port": "out", "target_node": "join_results", "target_port": "stocks"}, + + {"source_node": "join_results", "source_port": "out", "target_node": "analyze", "target_port": "joined"}, + {"source_node": "analyze", "source_port": "out", "target_node": "export_report", "target_port": "report"} + ] + } +} diff --git a/sample_projects/14_web_scraping_pipeline/main.py b/sample_projects/14_web_scraping_pipeline/main.py new file mode 100644 index 00000000..ae2b01b2 --- /dev/null +++ b/sample_projects/14_web_scraping_pipeline/main.py @@ -0,0 +1,98 @@ +""" +Web Scraping Pipeline — netrun feature showcase. + +Demonstrates all major netrun features in a single pipeline: + + [load_config] → [validate_config] → [broadcast_config] + / | \\ + [scrape_news] [scrape_weather] [scrape_stocks] + \\ | / + [join_results] + | + [analyze] + | + [setup_output] ─────────────────→ [export_report] → output_queue: results + +Features used: + - Async node functions on thread pool (shared event loop) + - ctx.state: caches expensive computation across retries + - depends_on: setup_output waits for load_config; export_report waits for setup_output + - resources: http_conn=2 limits concurrent scrapes to 2 of 3 + - Broadcast factory: fans config to 3 parallel scrapers + - Join factory: collects results from all 3 scrapers + - Retries: scrape_stocks simulates intermittent failure + - Structured logging: ctx.log() with key=value fields + - Node variables: site URLs configurable via ctx.vars + - Output queues: final report collected via flush_output_queue + - Signals: epoch_finished/epoch_failed on all nodes +""" + +import asyncio +import time +from pathlib import Path + +from netrun.core import Net, NetConfig + + +async def main(): + config_path = Path(__file__).parent / "main.netrun.json" + config = NetConfig.from_file(config_path) + + print("=" * 60) + print("Web Scraping Pipeline") + print("=" * 60) + print() + + start = time.monotonic() + + async with Net(config) as net: + # setup_output has no incoming data edge — inject a trigger packet. + # It's gated by depends_on: ["load_config"], so it won't start + # until load_config completes (which runs on init). + net.inject_data("setup_output", "trigger", ["go"]) + + # Run the full pipeline + await net.run_until_blocked() + + elapsed = time.monotonic() - start + + # Collect results + results = net.flush_output_queue("results") + + print() + print("=" * 60) + print("Pipeline Results") + print("=" * 60) + + if results: + report = results[0] + print(f" Total items scraped: {report['total_items']}") + print(f" Sources: {report['sources']}") + print(f" Breakdown: {report['summary']}") + else: + print(" No results (check dead letter queue)") + if net.dead_letter_queue: + for dl in net.dead_letter_queue: + print(f" FAILED: {dl['node_name']} — {dl['error']}") + + print() + print(f" Elapsed: {elapsed:.2f}s") + + # Show structured logs + print() + print("Structured Logs:") + for epoch_id, record in net._epochs.items(): + for log_entry in record.structured_logs: + print(f" [{record.node_name}] {log_entry.message} | {log_entry.fields}") + + # Show resource usage worked (at most 2 concurrent HTTP connections) + print() + print(f" Resource capacities: {net._resource_capacities}") + print(f" Resource usage (should be 0 after completion): {net._resource_usage}") + + print() + print("Done!") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/sample_projects/14_web_scraping_pipeline/nodes.py b/sample_projects/14_web_scraping_pipeline/nodes.py new file mode 100644 index 00000000..844a4218 --- /dev/null +++ b/sample_projects/14_web_scraping_pipeline/nodes.py @@ -0,0 +1,223 @@ +""" +Node functions for the web scraping pipeline. + +Demonstrates: +- Async node functions on thread pools (shared event loop) +- ctx.state for caching expensive data across retries +- ctx.log for structured logging +- ctx.vars for runtime configuration +- Simulated failures for retry testing + +Port names are derived from function parameter names (from_function factory). +""" + +import asyncio +import time +import random + + +# --------------------------------------------------------------------------- +# Stage 1: Configuration +# --------------------------------------------------------------------------- + +def load_config(ctx) -> dict: + """Load pipeline configuration from node variables. + + Demonstrates: ctx.vars, ctx.print, run_on_init + Output port: 'out' (single return) + """ + news_url = ctx.vars["news_url"] + weather_url = ctx.vars["weather_url"] + stocks_url = ctx.vars["stocks_url"] + # Node variables are already resolved by the time they reach the node. + # If they're NodeVariable objects, call resolve_value(); if already strings, use directly. + if hasattr(news_url, "resolve_value"): + news_url = news_url.resolve_value() + weather_url = weather_url.resolve_value() + stocks_url = stocks_url.resolve_value() + + config = { + "news_url": news_url, + "weather_url": weather_url, + "stocks_url": stocks_url, + "timestamp": time.time(), + } + + ctx.print(f"Config loaded: {len(config)} settings") + return config + + +def validate_config(ctx, config: dict) -> dict: + """Validate that the config has all required fields. + + Input port: 'config'. Output port: 'out'. + """ + required = {"news_url", "weather_url", "stocks_url"} + missing = required - set(config.keys()) + if missing: + raise ValueError(f"Missing config fields: {missing}") + + ctx.print(f"Config validated: all {len(required)} required fields present") + return config + + +# --------------------------------------------------------------------------- +# Stage 2: Setup (parallel path — demonstrates depends_on) +# --------------------------------------------------------------------------- + +def setup_output(ctx, trigger) -> dict: + """Prepare the output directory for export. + + Demonstrates: depends_on (waits for load_config before starting). + This node is NOT connected to load_config by a data edge — the + ordering is enforced purely by depends_on. + + Input port: 'trigger'. Output port: 'out'. + """ + output_dir = "/tmp/netrun_scraping_output" + ctx.print(f"Output directory prepared: {output_dir}") + return {"output_dir": output_dir, "ready": True} + + +# --------------------------------------------------------------------------- +# Stage 3: Async scrapers (thread pool, resources, retries) +# --------------------------------------------------------------------------- + +async def scrape_news(ctx, config: dict) -> dict: + """Scrape news headlines from a simulated news API. + + Demonstrates: async node function on thread pool, resources (http_conn), + structured logging (ctx.log) + Input port: 'config'. Output port: 'out'. + """ + url = config["news_url"] + ctx.print(f"Scraping news from {url}...") + + # Simulate async HTTP request (works on thread pools via shared event loop) + await asyncio.sleep(0.15) + + headlines = [ + {"title": "Tech stocks surge on AI optimism", "source": "Reuters"}, + {"title": "Climate summit reaches breakthrough", "source": "AP"}, + {"title": "New space telescope captures distant galaxy", "source": "NASA"}, + ] + + ctx.log("scrape_complete", source="news", url=url, items=len(headlines)) + ctx.print(f"News: {len(headlines)} headlines scraped") + return {"source": "news", "data": headlines} + + +async def scrape_weather(ctx, config: dict) -> dict: + """Scrape weather data from a simulated weather API. + + Demonstrates: async node function on thread pool, resources (http_conn) + Input port: 'config'. Output port: 'out'. + """ + url = config["weather_url"] + ctx.print(f"Scraping weather from {url}...") + + await asyncio.sleep(0.12) + + weather = [ + {"city": "London", "temp_c": 14, "condition": "cloudy"}, + {"city": "Tokyo", "temp_c": 22, "condition": "sunny"}, + {"city": "New York", "temp_c": 18, "condition": "rain"}, + ] + + ctx.log("scrape_complete", source="weather", url=url, items=len(weather)) + ctx.print(f"Weather: {len(weather)} cities scraped") + return {"source": "weather", "data": weather} + + +async def scrape_stocks(ctx, config: dict) -> dict: + """Scrape stock prices from a simulated stock API. + + Demonstrates: async on thread pool, resources, retries with intermittent + failure. First attempt fails ~50% of the time to exercise retry logic. + Input port: 'config'. Output port: 'out'. + """ + url = config["stocks_url"] + ctx.print(f"Scraping stocks from {url} (attempt {ctx.retry_count + 1})...") + + await asyncio.sleep(0.10) + + # Simulate intermittent API failure on first attempt + if ctx.retry_count == 0 and random.random() < 0.5: + ctx.print("Stock API returned 503 — will retry") + raise ConnectionError(f"HTTP 503 from {url}") + + stocks = [ + {"symbol": "AAPL", "price": 187.50, "change": +1.2}, + {"symbol": "GOOGL", "price": 141.80, "change": -0.5}, + {"symbol": "MSFT", "price": 378.20, "change": +2.1}, + ] + + ctx.log("scrape_complete", source="stocks", url=url, items=len(stocks), + retries=ctx.retry_count) + ctx.print(f"Stocks: {len(stocks)} prices scraped") + return {"source": "stocks", "data": stocks} + + +# --------------------------------------------------------------------------- +# Stage 4: Analysis (retries + ctx.state) +# --------------------------------------------------------------------------- + +def analyze(ctx, joined: dict) -> dict: + """Analyze combined scraping results. + + Demonstrates: ctx.state (caches expensive computation across retries), + structured logging (ctx.log), retries. + + On first attempt, builds an analysis from the joined data and stores it + in ctx.state. On retry, reuses the cached result. + Input port: 'joined'. Output port: 'out'. + """ + # Use ctx.state to cache the expensive computation across retries + if "analysis_result" not in ctx.state: + ctx.print("Computing analysis (first time — will cache in ctx.state)...") + + total_items = 0 + sources = [] + summary = {} + + for port_name, value in joined.items(): + if isinstance(value, dict) and "data" in value: + total_items += len(value["data"]) + sources.append(value["source"]) + summary[value["source"]] = len(value["data"]) + + result = { + "total_items": total_items, + "sources": sources, + "source_count": len(sources), + "summary": summary, + } + + ctx.state["analysis_result"] = result + ctx.print(f"Analysis cached: {total_items} items from {len(sources)} sources") + else: + result = ctx.state["analysis_result"] + ctx.print(f"Reusing cached analysis from ctx.state (retry {ctx.retry_count})") + + ctx.log("analysis_complete", + total_items=result["total_items"], + sources=result["source_count"], + retry_count=ctx.retry_count) + + return result + + +# --------------------------------------------------------------------------- +# Stage 5: Export (depends_on setup_output) +# --------------------------------------------------------------------------- + +def export_report(ctx, report: dict) -> dict: + """Export the analysis report. + + Demonstrates: depends_on (waits for setup_output before starting), + output queue (results pushed to 'results' queue). + Input port: 'report'. Output port: 'out'. + """ + ctx.print(f"Exporting report: {report['total_items']} items from {report['source_count']} sources") + ctx.print(f" Summary: {report['summary']}") + return report From 12be859b5dd01001b8b1eacb22f7953df7438892 Mon Sep 17 00:00:00 2001 From: Lukas Kikuchi Date: Mon, 27 Apr 2026 18:06:31 +0100 Subject: [PATCH 10/15] refactor(netrun): use thread-local for shared loop, add multiprocess to sample MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the fragile pattern of mutating func_preprocessor._shared_loop from worker threads with a clean thread-local (_worker_state.shared_loop). Why: The preprocessor must stay picklable for multiprocess pools. Previously, thread pool workers set _shared_loop on the shared preprocessor instance, which made it unpicklable if a multiprocess pool tried to serialize it later. The __getstate__ band-aid was masking this design issue. Now: _worker_state (threading.local) holds the shared loop. The preprocessor's call_for_sync_worker reads it from thread-local when dispatching async functions. No execution state stored on the preprocessor — it stays fully picklable. Sample project updated: - Added multiprocess pool ("compute") for the analyze node - Added retain_epoch_logs for full log visibility - Added net.logs.print_all() showing all node output including multiprocess - All 3 pool types now demonstrated: main, thread, multiprocess Co-Authored-By: Claude Opus 4.6 (1M context) --- netrun/pts/netrun/05_execution_manager.pct.py | 22 +++++++++++-- .../netrun/06_net/01_net/00_context.pct.py | 16 ++++++---- netrun/src/netrun/execution_manager.py | 32 +++++++++++++------ netrun/src/netrun/net/_net/_context.py | 16 ++++++---- .../14_web_scraping_pipeline/main.netrun.json | 6 ++-- .../14_web_scraping_pipeline/main.py | 9 ++++++ .../14_web_scraping_pipeline/nodes.py | 7 +++- 7 files changed, 80 insertions(+), 28 deletions(-) diff --git a/netrun/pts/netrun/05_execution_manager.pct.py b/netrun/pts/netrun/05_execution_manager.pct.py index 5aa88016..9f5bf4c9 100644 --- a/netrun/pts/netrun/05_execution_manager.pct.py +++ b/netrun/pts/netrun/05_execution_manager.pct.py @@ -104,6 +104,20 @@ def _func_runner( else: return func(*call_args, **kwargs) +# %% [markdown] +# ## Worker thread-local state + +# %% +#|export +_worker_state = threading.local() +"""Thread-local state for worker threads. + +Attributes set by _worker_func on startup: + shared_loop: The shared event loop for async function dispatch. + Read by NetFuncPreprocessor.call_for_sync_worker() to submit + async user functions via run_coroutine_threadsafe(). +""" + # %% [markdown] # ## Helpers @@ -181,9 +195,11 @@ def _worker_func( shared_loop: Shared event loop for async function dispatch. Async user functions are submitted to this loop via run_coroutine_threadsafe(). """ - # Set shared loop on preprocessor so its sync wrapper can dispatch async functions - if func_preprocessor is not None and shared_loop is not None: - func_preprocessor._shared_loop = shared_loop + # Set shared loop on thread-local so the preprocessor's sync wrapper can + # dispatch async functions via run_coroutine_threadsafe(). This avoids + # mutating the preprocessor instance (which must stay picklable for + # multiprocess pools). + _worker_state.shared_loop = shared_loop registered_functions: dict[str, Callable[..., Awaitable] | Callable[..., None]] = {} diff --git a/netrun/pts/netrun/06_net/01_net/00_context.pct.py b/netrun/pts/netrun/06_net/01_net/00_context.pct.py index b0e09902..335b75e7 100644 --- a/netrun/pts/netrun/06_net/01_net/00_context.pct.py +++ b/netrun/pts/netrun/06_net/01_net/00_context.pct.py @@ -37,6 +37,7 @@ from netrun.net.config._nodes import resolve_effective_exec_field, INHERITABLE_EXEC_FIELDS from netrun._iutils import get_timestamp_utc from netrun.packets import LazyPacketValueSpec +from netrun.execution_manager import _worker_state # %% [markdown] # ## Net Protocol Keys @@ -1210,8 +1211,12 @@ class NetFuncPreprocessor: Provides two wrapper modes: - __call__: returns async wrapper (for SingleWorkerPool / async contexts) - call_for_sync_worker: returns sync wrapper (for ThreadPool / MultiprocessPool) - Async user functions are dispatched to _shared_loop via run_coroutine_threadsafe. + Async user functions are dispatched to the shared event loop (read from + thread-local _worker_state.shared_loop) via run_coroutine_threadsafe. Sync user functions run directly in the calling thread. + + No execution state is stored on this object — it stays fully picklable. + The shared event loop reference lives on the worker thread-local, not here. """ def __init__( @@ -1226,8 +1231,6 @@ def __init__( self._node_configs = node_configs # Worker-local cache for resolved factory functions self._resolved_funcs: dict[str, Callable] = {} - # Shared event loop for async dispatch (set by worker on startup) - self._shared_loop: asyncio.AbstractEventLoop | None = None def _resolve_factory(self, node_name: str) -> Callable | None: """Resolve factory function on worker (lazy). @@ -1415,13 +1418,14 @@ def wrapped( try: ctx._validate_input_packets(packets) if asyncio.iscoroutinefunction(actual_func): - if preprocessor_self._shared_loop is None: + shared_loop = getattr(_worker_state, 'shared_loop', None) + if shared_loop is None: raise RuntimeError( f"Async node function '{node_name}' requires a shared event loop, " - "but none was set on the preprocessor. This is a netrun internal error." + "but none was set on the worker thread. This is a netrun internal error." ) coro = actual_func(ctx, packets) - future = asyncio.run_coroutine_threadsafe(coro, preprocessor_self._shared_loop) + future = asyncio.run_coroutine_threadsafe(coro, shared_loop) func_result = future.result() else: func_result = actual_func(ctx, packets) diff --git a/netrun/src/netrun/execution_manager.py b/netrun/src/netrun/execution_manager.py index d2ef6fc5..3f0114e3 100644 --- a/netrun/src/netrun/execution_manager.py +++ b/netrun/src/netrun/execution_manager.py @@ -79,6 +79,16 @@ def _func_runner( return func(*call_args, **kwargs) # %% pts/netrun/05_execution_manager.pct.py 8 +_worker_state = threading.local() +"""Thread-local state for worker threads. + +Attributes set by _worker_func on startup: + shared_loop: The shared event loop for async function dispatch. + Read by NetFuncPreprocessor.call_for_sync_worker() to submit + async user functions via run_coroutine_threadsafe(). +""" + +# %% pts/netrun/05_execution_manager.pct.py 10 def _convert_to_str_if_not_serializable(obj: Any) -> tuple[bool, Any]: """Convert an object to string if it's not pickle-serializable. @@ -91,7 +101,7 @@ def _convert_to_str_if_not_serializable(obj: Any) -> tuple[bool, Any]: except (pickle.PicklingError, TypeError, AttributeError): return (True, str(obj)) -# %% pts/netrun/05_execution_manager.pct.py 10 +# %% pts/netrun/05_execution_manager.pct.py 12 class ExecutionManagerProtocolKeys(Enum): RUN = "exec-manager:run" """ @@ -123,7 +133,7 @@ class ExecutionManagerProtocolKeys(Enum): Args: msg_id """ -# %% pts/netrun/05_execution_manager.pct.py 11 +# %% pts/netrun/05_execution_manager.pct.py 13 def _worker_func( is_in_main_process: bool, channel, @@ -144,9 +154,11 @@ def _worker_func( shared_loop: Shared event loop for async function dispatch. Async user functions are submitted to this loop via run_coroutine_threadsafe(). """ - # Set shared loop on preprocessor so its sync wrapper can dispatch async functions - if func_preprocessor is not None and shared_loop is not None: - func_preprocessor._shared_loop = shared_loop + # Set shared loop on thread-local so the preprocessor's sync wrapper can + # dispatch async functions via run_coroutine_threadsafe(). This avoids + # mutating the preprocessor instance (which must stay picklable for + # multiprocess pools). + _worker_state.shared_loop = shared_loop registered_functions: dict[str, Callable[..., Awaitable] | Callable[..., None]] = {} @@ -213,7 +225,7 @@ def _worker_func( else: raise ValueError(f"Unknown execution manager protocol key: '{key}'.") -# %% pts/netrun/05_execution_manager.pct.py 12 +# %% pts/netrun/05_execution_manager.pct.py 14 async def _async_worker_func( channel, worker_id, @@ -306,7 +318,7 @@ async def _handle_run(msg_id, func, send_channel, args, kwargs): if pending_tasks: await asyncio.gather(*pending_tasks, return_exceptions=True) -# %% pts/netrun/05_execution_manager.pct.py 14 +# %% pts/netrun/05_execution_manager.pct.py 16 def remote_execution_manager_worker( channel, worker_id: int, @@ -323,7 +335,7 @@ def remote_execution_manager_worker( shared_loop=shared_loop, ) -# %% pts/netrun/05_execution_manager.pct.py 15 +# %% pts/netrun/05_execution_manager.pct.py 17 def create_execution_manager_server( worker_name: str = "execution_manager", func_preprocessor: Callable | None = None, @@ -393,7 +405,7 @@ def create_execution_manager_server( server.register_worker(worker_name, worker_fn) return server -# %% pts/netrun/05_execution_manager.pct.py 17 +# %% pts/netrun/05_execution_manager.pct.py 19 @dataclass class JobResult: """Result of a job execution.""" @@ -422,7 +434,7 @@ class RunAllocationMethod(Enum): RANDOM = "random" LEAST_BUSY = "least-busy" -# %% pts/netrun/05_execution_manager.pct.py 18 +# %% pts/netrun/05_execution_manager.pct.py 20 PoolType = ThreadPool | MultiprocessPool | SingleWorkerPool | RemotePoolClient class ExecutionManager: diff --git a/netrun/src/netrun/net/_net/_context.py b/netrun/src/netrun/net/_net/_context.py index 9338860c..2ab3e94d 100644 --- a/netrun/src/netrun/net/_net/_context.py +++ b/netrun/src/netrun/net/_net/_context.py @@ -19,6 +19,7 @@ from ...net.config._nodes import resolve_effective_exec_field, INHERITABLE_EXEC_FIELDS from ..._iutils import get_timestamp_utc from ...packets import LazyPacketValueSpec +from ...execution_manager import _worker_state # %% pts/netrun/06_net/01_net/00_context.pct.py 5 class NetProtocolKeys(Enum): @@ -1129,8 +1130,12 @@ class NetFuncPreprocessor: Provides two wrapper modes: - __call__: returns async wrapper (for SingleWorkerPool / async contexts) - call_for_sync_worker: returns sync wrapper (for ThreadPool / MultiprocessPool) - Async user functions are dispatched to _shared_loop via run_coroutine_threadsafe. + Async user functions are dispatched to the shared event loop (read from + thread-local _worker_state.shared_loop) via run_coroutine_threadsafe. Sync user functions run directly in the calling thread. + + No execution state is stored on this object — it stays fully picklable. + The shared event loop reference lives on the worker thread-local, not here. """ def __init__( @@ -1145,8 +1150,6 @@ def __init__( self._node_configs = node_configs # Worker-local cache for resolved factory functions self._resolved_funcs: dict[str, Callable] = {} - # Shared event loop for async dispatch (set by worker on startup) - self._shared_loop: asyncio.AbstractEventLoop | None = None def _resolve_factory(self, node_name: str) -> Callable | None: """Resolve factory function on worker (lazy). @@ -1334,13 +1337,14 @@ def wrapped( try: ctx._validate_input_packets(packets) if asyncio.iscoroutinefunction(actual_func): - if preprocessor_self._shared_loop is None: + shared_loop = getattr(_worker_state, 'shared_loop', None) + if shared_loop is None: raise RuntimeError( f"Async node function '{node_name}' requires a shared event loop, " - "but none was set on the preprocessor. This is a netrun internal error." + "but none was set on the worker thread. This is a netrun internal error." ) coro = actual_func(ctx, packets) - future = asyncio.run_coroutine_threadsafe(coro, preprocessor_self._shared_loop) + future = asyncio.run_coroutine_threadsafe(coro, shared_loop) func_result = future.result() else: func_result = actual_func(ctx, packets) diff --git a/sample_projects/14_web_scraping_pipeline/main.netrun.json b/sample_projects/14_web_scraping_pipeline/main.netrun.json index ba3a0c1b..a703b5d8 100644 --- a/sample_projects/14_web_scraping_pipeline/main.netrun.json +++ b/sample_projects/14_web_scraping_pipeline/main.netrun.json @@ -5,7 +5,8 @@ "pools": { "main": {"spec": {"type": "main"}}, - "scrape": {"spec": {"type": "thread", "num_workers": 3}} + "scrape": {"spec": {"type": "thread", "num_workers": 3}}, + "compute": {"spec": {"type": "multiprocess", "num_processes": 1, "threads_per_process": 1}} }, "resources": { @@ -21,6 +22,7 @@ "retries": 0, "print_echo_stdout": true, "propagate_exceptions": true, + "retain_epoch_logs": true, "default_signals": ["epoch_finished", "epoch_failed"], @@ -103,7 +105,7 @@ "factory": "netrun.node_factories.from_function", "factory_args": {"func": "nodes.analyze"}, "execution_config": { - "pools": ["main"], + "pools": ["compute"], "retries": 2, "retry_wait": 0.0 } diff --git a/sample_projects/14_web_scraping_pipeline/main.py b/sample_projects/14_web_scraping_pipeline/main.py index ae2b01b2..bbff3d57 100644 --- a/sample_projects/14_web_scraping_pipeline/main.py +++ b/sample_projects/14_web_scraping_pipeline/main.py @@ -78,6 +78,14 @@ async def main(): print() print(f" Elapsed: {elapsed:.2f}s") + # Show all node print logs (includes multiprocess workers whose + # ctx.print output doesn't echo to stdout directly) + # Print all captured node output (includes multiprocess workers + # whose ctx.print doesn't echo to terminal directly) + print() + print("All Node Output (via ctx.print):") + net.logs.print_all() + # Show structured logs print() print("Structured Logs:") @@ -87,6 +95,7 @@ async def main(): # Show resource usage worked (at most 2 concurrent HTTP connections) print() + print(f" Pool types: main (SingleWorkerPool), scrape (ThreadPool x3), compute (MultiprocessPool x1)") print(f" Resource capacities: {net._resource_capacities}") print(f" Resource usage (should be 0 after completion): {net._resource_usage}") diff --git a/sample_projects/14_web_scraping_pipeline/nodes.py b/sample_projects/14_web_scraping_pipeline/nodes.py index 844a4218..571108af 100644 --- a/sample_projects/14_web_scraping_pipeline/nodes.py +++ b/sample_projects/14_web_scraping_pipeline/nodes.py @@ -169,7 +169,12 @@ def analyze(ctx, joined: dict) -> dict: structured logging (ctx.log), retries. On first attempt, builds an analysis from the joined data and stores it - in ctx.state. On retry, reuses the cached result. + in ctx.state. On retry, reuses the cached result instead of recomputing. + + Note: ctx.state works best with thread pools (same process, shared memory). + With multiprocess pools, the dict is pickled per retry — mutations within + a single attempt work, but cross-retry persistence requires thread pools. + Input port: 'joined'. Output port: 'out'. """ # Use ctx.state to cache the expensive computation across retries From 43710cb77fb8846bb15e0fb0dccc96398dfb1ccd Mon Sep 17 00:00:00 2001 From: Lukas Kikuchi Date: Mon, 27 Apr 2026 23:47:25 +0100 Subject: [PATCH 11/15] clean(netrun): remove dead ExecutionManager protocol weight MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove dead code and simplify the ExecutionManager protocol: - Remove NetProtocolKeys enum (defined channel-based protocol that was never implemented — deferred actions replaced it entirely) - Remove send_channel feature (Net always passed False; bidirectional channel during execution was never used) - Merge UP_RUN_STARTED into UP_RUN_RESPONSE (eliminates one protocol message round-trip per execution; started timestamp now included in the single response) Co-Authored-By: Claude Opus 4.6 (1M context) --- netrun/pts/netrun/05_execution_manager.pct.py | 82 +--- .../netrun/06_net/01_net/00_context.pct.py | 42 -- netrun/pts/netrun/06_net/01_net/02_net.pct.py | 1 - .../test_execution_manager_async.pct.py | 30 +- ...test_execution_manager_multiprocess.pct.py | 32 +- .../test_execution_manager_remote.pct.py | 60 +-- .../test_execution_manager_thread.pct.py | 52 +- netrun/pts/tests/06_net/test_net.pct.py | 32 -- netrun/src/netrun/execution_manager.py | 78 +-- netrun/src/netrun/net/_net/__init__.py | 1 - netrun/src/netrun/net/_net/_context.py | 54 +-- netrun/src/netrun/net/_net/_net.py | 1 - .../test_execution_manager_async.py | 30 +- .../test_execution_manager_multiprocess.py | 32 +- .../test_execution_manager_remote.py | 60 +-- .../test_execution_manager_thread.py | 52 +- netrun/src/tests/net/test_net.py | 445 +++++++++--------- 17 files changed, 413 insertions(+), 671 deletions(-) diff --git a/netrun/pts/netrun/05_execution_manager.pct.py b/netrun/pts/netrun/05_execution_manager.pct.py index 9f5bf4c9..ad35b870 100644 --- a/netrun/pts/netrun/05_execution_manager.pct.py +++ b/netrun/pts/netrun/05_execution_manager.pct.py @@ -36,7 +36,6 @@ import random import functools -from netrun.rpc.base import RPCChannel from netrun.pool.base import POOL_UP_ERROR_KEYS, _check_error_and_raise from netrun.pool.thread import ThreadPool from netrun.pool.multiprocess import MultiprocessPool @@ -51,29 +50,19 @@ # %% #|exporti async def _async_func_runner( - channel: RPCChannel, func: Callable[..., Awaitable[Any]], - send_channel: bool, args: tuple, kwargs: dict, ) -> Any: if asyncio.iscoroutinefunction(func): - if send_channel: - return await func(channel, *args, **kwargs) - else: - return await func(*args, **kwargs) + return await func(*args, **kwargs) else: - if send_channel: - return func(channel, *args, **kwargs) - else: - return func(*args, **kwargs) + return func(*args, **kwargs) # %% #|exporti def _func_runner( - channel: RPCChannel, func: Callable[..., Any], - send_channel: bool, args: tuple, kwargs: dict, shared_loop: asyncio.AbstractEventLoop | None = None, @@ -84,13 +73,8 @@ def _func_runner( async dispatch internally. When called directly without a preprocessor, async functions are submitted to the shared event loop. """ - if send_channel: - call_args = (channel, *args) - else: - call_args = args - if asyncio.iscoroutinefunction(func): - coro = func(*call_args, **kwargs) + coro = func(*args, **kwargs) if shared_loop is not None: future = asyncio.run_coroutine_threadsafe(coro, shared_loop) return future.result() @@ -102,7 +86,7 @@ def _func_runner( finally: loop.close() else: - return func(*call_args, **kwargs) + return func(*args, **kwargs) # %% [markdown] # ## Worker thread-local state @@ -146,19 +130,13 @@ class ExecutionManagerProtocolKeys(Enum): RUN = "exec-manager:run" """ Run a function. - Args: msg_id, func_import_path_or_key, run_id, send_channel, args, kwargs - """ - - UP_RUN_STARTED = "exec-manager-up:run-started" - """ - Notification from the worker that a run has been submitted and started. - Args: msg_id, timestamp_utc_started + Args: msg_id, func_import_path_or_key, run_id, args, kwargs """ UP_RUN_RESPONSE = "exec-manager-up:run-response" """ Response to RUN from the worker. - Args: msg_id, converted_to_str, _res + Args: msg_id, timestamp_utc_started, timestamp_utc_completed, converted_to_str, _res """ SEND_FUNCTION = "exec-manager:send-function" @@ -207,9 +185,8 @@ def _worker_func( key, data = channel.recv() # RUN if key == ExecutionManagerProtocolKeys.RUN.value: - msg_id, func_import_path_or_key, run_id, send_channel, args, kwargs = data + msg_id, func_import_path_or_key, run_id, args, kwargs = data timestamp_utc_started = None - started_sent = False try: if func_import_path_or_key in registered_functions: func = registered_functions[func_import_path_or_key] @@ -222,12 +199,8 @@ def _worker_func( func = func_preprocessor.call_for_sync_worker(func) timestamp_utc_started = get_timestamp_utc() - channel.send(ExecutionManagerProtocolKeys.UP_RUN_STARTED.value, (msg_id, timestamp_utc_started)) - started_sent = True res = _func_runner( - channel=channel, func=func, - send_channel=send_channel, args=args, kwargs=kwargs, shared_loop=shared_loop, @@ -235,10 +208,7 @@ def _worker_func( # Call done callback if provided if func_done_callback is not None: - if send_channel: - func_done_callback(channel, *args, **kwargs, result=res) - else: - func_done_callback(*args, **kwargs, result=res) + func_done_callback(*args, **kwargs, result=res) timestamp_utc_completed = get_timestamp_utc() if is_in_main_process: @@ -253,8 +223,6 @@ def _worker_func( if timestamp_utc_started is None: timestamp_utc_started = timestamp_utc_error try: - if not started_sent: - channel.send(ExecutionManagerProtocolKeys.UP_RUN_STARTED.value, (msg_id, timestamp_utc_started)) channel.send(ExecutionManagerProtocolKeys.UP_RUN_RESPONSE.value, (msg_id, timestamp_utc_started, timestamp_utc_error, False, e)) except Exception: pass # Worker loop must survive @@ -288,26 +256,19 @@ async def _async_worker_func( """ registered_functions: dict[str, Callable[..., Awaitable] | Callable[..., None]] = {} - async def _handle_run(msg_id, func, send_channel, args, kwargs): + async def _handle_run(msg_id, func, args, kwargs): """Execute a single RUN request and send the response.""" timestamp_utc_started = get_timestamp_utc() try: - await channel.send(ExecutionManagerProtocolKeys.UP_RUN_STARTED.value, (msg_id, timestamp_utc_started)) - res = await _async_func_runner( - channel=channel, func=func, - send_channel=send_channel, args=args, kwargs=kwargs, ) # Call done callback if provided if func_done_callback is not None: - if send_channel: - callback_result = func_done_callback(channel, *args, **kwargs, result=res) - else: - callback_result = func_done_callback(*args, **kwargs, result=res) + callback_result = func_done_callback(*args, **kwargs, result=res) # Await if the callback is async if asyncio.iscoroutine(callback_result): await callback_result @@ -331,7 +292,7 @@ async def _handle_run(msg_id, func, send_channel, args, kwargs): key, data = await channel.recv() # RUN if key == ExecutionManagerProtocolKeys.RUN.value: - msg_id, func_import_path_or_key, run_id, send_channel, args, kwargs = data + msg_id, func_import_path_or_key, run_id, args, kwargs = data if func_import_path_or_key in registered_functions: func = registered_functions[func_import_path_or_key] else: @@ -343,7 +304,7 @@ async def _handle_run(msg_id, func, send_channel, args, kwargs): func = func_preprocessor(func) # Spawn as concurrent task instead of awaiting inline - task = asyncio.create_task(_handle_run(msg_id, func, send_channel, args, kwargs)) + task = asyncio.create_task(_handle_run(msg_id, func, args, kwargs)) pending_tasks.add(task) task.add_done_callback(pending_tasks.discard) # SEND_FUNCTION @@ -682,7 +643,6 @@ async def run( pool_id: str, worker_id: str, func_import_path_or_key: str, - send_channel: bool, func_args, func_kwargs, ) -> JobResult: @@ -693,7 +653,6 @@ async def run( pool_id: The ID of the pool to run the function in. worker_id: The ID of the worker to run the function on. func_import_path_or_key: The import path or key of the function to run (for the latter, use send_function to register the function first) - send_channel: Whether to send the worker RPC channel to the function. func_args: The arguments to pass to the function. func_kwargs: The keyword arguments to pass to the function. @@ -723,25 +682,22 @@ async def run( pool_id=pool_id, worker_id=worker_id, key=ExecutionManagerProtocolKeys.RUN.value, - data=(func_import_path_or_key, run_id, send_channel, func_args, func_kwargs), + data=(func_import_path_or_key, run_id, func_args, func_kwargs), ) - # Wait for UP_RUN_STARTED - started_msg = await self._recv_msg(pool_id, msg_id, expect=ExecutionManagerProtocolKeys.UP_RUN_STARTED, close_msg_queue=False) - job_info.timestamp_utc_started = started_msg.data[0] # timestamp_utc_started - - # Wait for UP_RUN_RESPONSE + # Wait for UP_RUN_RESPONSE (single response containing all timestamps) msg = await self._recv_msg(pool_id, msg_id, expect=ExecutionManagerProtocolKeys.UP_RUN_RESPONSE, close_msg_queue=True) msg_id = None # Already cleaned up by close_msg_queue=True self._worker_jobs[(pool_id, worker_id)].remove(job_info) timestamp_utc_started, timestamp_utc_completed, converted_to_str, _res = msg.data + job_info.timestamp_utc_started = timestamp_utc_started if isinstance(_res, Exception): raise _res return JobResult( timestamp_utc_submitted=job_info.timestamp_utc_submitted, - timestamp_utc_started=job_info.timestamp_utc_started, + timestamp_utc_started=timestamp_utc_started, timestamp_utc_completed=timestamp_utc_completed, func_import_path_or_key=job_info.func_import_path_or_key, pool_id=job_info.pool_id, @@ -764,7 +720,6 @@ async def run_allocate( pool_worker_ids: list[str | tuple[str, str]], allocation_method: RunAllocationMethod, func_import_path_or_key: str, - send_channel: bool, func_args, func_kwargs, ) -> JobResult: @@ -810,7 +765,6 @@ def active_job_count(pool_worker: tuple[str, str]) -> int: pool_id=pool_id, worker_id=worker_id, func_import_path_or_key=func_import_path_or_key, - send_channel=send_channel, func_args=func_args, func_kwargs=func_kwargs, ) @@ -1032,7 +986,7 @@ def example_add(a: int, b: int) -> int: pool_id="workers", worker_id=0, func_import_path_or_key="add", - send_channel=False, + func_args=(3, 4), func_kwargs={}, ) @@ -1077,7 +1031,7 @@ def example_multiply(x: int, y: int) -> int: pool_worker_ids=["compute"], # Use all workers in "compute" pool allocation_method=RunAllocationMethod.ROUND_ROBIN, func_import_path_or_key="multiply", - send_channel=False, + func_args=(i, i + 1), func_kwargs={}, ) diff --git a/netrun/pts/netrun/06_net/01_net/00_context.pct.py b/netrun/pts/netrun/06_net/01_net/00_context.pct.py index 335b75e7..c02b6f65 100644 --- a/netrun/pts/netrun/06_net/01_net/00_context.pct.py +++ b/netrun/pts/netrun/06_net/01_net/00_context.pct.py @@ -26,7 +26,6 @@ import uuid from dataclasses import dataclass, field from datetime import datetime, timezone -from enum import Enum from typing import Any, NoReturn, get_origin from collections.abc import Callable import importlib @@ -39,47 +38,6 @@ from netrun.packets import LazyPacketValueSpec from netrun.execution_manager import _worker_state -# %% [markdown] -# ## Net Protocol Keys -# -# Communication keys for Net <-> Worker messages. - -# %% -#|export -class NetProtocolKeys(Enum): - """Protocol keys for Net <-> Worker communication.""" - - # Upstream (Worker -> Net) - sent via channel when send_channel=True - UP_CREATE_PACKET = "net:create-packet" - """Create a new packet. Args: (epoch_id, value_or_lazy)""" - - UP_CREATE_PACKET_RESPONSE = "net:create-packet-response" - """Response with packet ID. Args: (packet_id,)""" - - UP_CONSUME_PACKET = "net:consume-packet" - """Consume a packet. Args: (epoch_id, packet_id)""" - - UP_CONSUME_PACKET_RESPONSE = "net:consume-packet-response" - """Response with packet value. Args: (value,)""" - - UP_LOAD_OUTPUT_PORT = "net:load-output-port" - """Load packet into output port. Args: (epoch_id, port_name, packet_id)""" - - UP_LOAD_OUTPUT_PORT_RESPONSE = "net:load-output-port-response" - """Acknowledgement. Args: ()""" - - UP_SEND_OUTPUT_SALVO = "net:send-salvo" - """Send output salvo. Args: (epoch_id, salvo_condition_name)""" - - UP_SEND_OUTPUT_SALVO_RESPONSE = "net:send-salvo-response" - """Acknowledgement. Args: ()""" - - UP_CANCEL_EPOCH = "net:cancel-epoch" - """Cancel the current epoch. Args: (epoch_id,)""" - - UP_PRINT_BUFFER = "net:print-buffer" - """Send captured print output. Args: (epoch_id, buffer: list[str])""" - # %% [markdown] # ## Structured Logging Data Models # diff --git a/netrun/pts/netrun/06_net/01_net/02_net.pct.py b/netrun/pts/netrun/06_net/01_net/02_net.pct.py index ab8e8c86..b97293c4 100644 --- a/netrun/pts/netrun/06_net/01_net/02_net.pct.py +++ b/netrun/pts/netrun/06_net/01_net/02_net.pct.py @@ -2430,7 +2430,6 @@ async def _execute_epoch_with_retry( pool_worker_ids=config.pools, allocation_method=allocation_method, func_import_path_or_key=func_key, - send_channel=False, # Deferred mode doesn't need channel func_args=(epoch_id, node_name, packets, packet_values), func_kwargs={ "retry_count": retry_count, diff --git a/netrun/pts/tests/05_execution_manager/test_execution_manager_async.pct.py b/netrun/pts/tests/05_execution_manager/test_execution_manager_async.pct.py index 8858790e..2991805a 100644 --- a/netrun/pts/tests/05_execution_manager/test_execution_manager_async.pct.py +++ b/netrun/pts/tests/05_execution_manager/test_execution_manager_async.pct.py @@ -169,7 +169,7 @@ async def test_send_function_and_run(): pool_id="pool", worker_id=0, func_import_path_or_key="add", - send_channel=False, + func_args=(3, 4), func_kwargs={}, ) @@ -200,7 +200,7 @@ async def test_send_function_to_pool(): pool_id="pool", worker_id=0, func_import_path_or_key="multiply", - send_channel=False, + func_args=(5, 10), func_kwargs={}, ) @@ -229,7 +229,7 @@ async def test_job_result_timestamps(): pool_id="pool", worker_id=0, func_import_path_or_key="slow", - send_channel=False, + func_args=(0.1,), func_kwargs={}, ) @@ -260,7 +260,7 @@ async def test_non_serializable_result(): pool_id="pool", worker_id=0, func_import_path_or_key="nonserialized", - send_channel=False, + func_args=(), func_kwargs={}, ) @@ -294,7 +294,7 @@ async def test_function_with_kwargs(): pool_id="pool", worker_id=0, func_import_path_or_key="kwargs_fn", - send_channel=False, + func_args=(1,), func_kwargs={}, ) @@ -305,7 +305,7 @@ async def test_function_with_kwargs(): pool_id="pool", worker_id=0, func_import_path_or_key="kwargs_fn", - send_channel=False, + func_args=(5,), func_kwargs={"b": 20, "c": 200}, ) @@ -336,7 +336,7 @@ async def test_round_robin_allocation(): pool_worker_ids=["pool"], allocation_method=RunAllocationMethod.ROUND_ROBIN, func_import_path_or_key="add", - send_channel=False, + func_args=(i, 1), func_kwargs={}, ) @@ -365,7 +365,7 @@ async def test_empty_workers_raises(): pool_worker_ids=[], allocation_method=RunAllocationMethod.ROUND_ROBIN, func_import_path_or_key="add", - send_channel=False, + func_args=(1, 2), func_kwargs={}, ) @@ -414,7 +414,7 @@ async def test_multiple_pools(): pool_id="pool_a", worker_id=0, func_import_path_or_key="add", - send_channel=False, + func_args=(5, 3), func_kwargs={}, ) @@ -424,7 +424,7 @@ async def test_multiple_pools(): pool_id="pool_b", worker_id=0, func_import_path_or_key="multiply", - send_channel=False, + func_args=(4, 7), func_kwargs={}, ) @@ -456,7 +456,7 @@ async def test_async_function(): pool_id="pool", worker_id=0, func_import_path_or_key="async_add", - send_channel=False, + func_args=(10, 20), func_kwargs={}, ) @@ -485,7 +485,7 @@ async def test_pool_class_directly(): pool_id="pool", worker_id=0, func_import_path_or_key="add", - send_channel=False, + func_args=(100, 200), func_kwargs={}, ) @@ -531,7 +531,7 @@ async def slow_async_func(delay: float) -> str: pool_id="pool", worker_id=0, func_import_path_or_key="slow", - send_channel=False, + func_args=(0.2,), func_kwargs={}, ) @@ -579,7 +579,7 @@ async def test_worker_exception_does_not_hang(): pool_id="pool", worker_id=0, func_import_path_or_key="error_fn", - send_channel=False, + func_args=(), func_kwargs={}, ), @@ -592,7 +592,7 @@ async def test_worker_exception_does_not_hang(): pool_id="pool", worker_id=0, func_import_path_or_key="add", - send_channel=False, + func_args=(10, 20), func_kwargs={}, ) diff --git a/netrun/pts/tests/05_execution_manager/test_execution_manager_multiprocess.pct.py b/netrun/pts/tests/05_execution_manager/test_execution_manager_multiprocess.pct.py index a4964f2c..1d455284 100644 --- a/netrun/pts/tests/05_execution_manager/test_execution_manager_multiprocess.pct.py +++ b/netrun/pts/tests/05_execution_manager/test_execution_manager_multiprocess.pct.py @@ -167,7 +167,7 @@ async def test_send_function_and_run(): pool_id="pool", worker_id=0, func_import_path_or_key="add", - send_channel=False, + func_args=(3, 4), func_kwargs={}, ) @@ -199,7 +199,7 @@ async def test_send_function_to_pool(): pool_id="pool", worker_id=worker_id, func_import_path_or_key="multiply", - send_channel=False, + func_args=(worker_id + 1, 10), func_kwargs={}, ) @@ -229,7 +229,7 @@ async def test_job_result_timestamps(): pool_id="pool", worker_id=0, func_import_path_or_key="slow", - send_channel=False, + func_args=(0.1,), func_kwargs={}, ) @@ -264,7 +264,7 @@ async def test_function_with_kwargs(): pool_id="pool", worker_id=0, func_import_path_or_key="kwargs_fn", - send_channel=False, + func_args=(1,), func_kwargs={}, ) @@ -275,7 +275,7 @@ async def test_function_with_kwargs(): pool_id="pool", worker_id=0, func_import_path_or_key="kwargs_fn", - send_channel=False, + func_args=(5,), func_kwargs={"b": 20, "c": 200}, ) @@ -306,7 +306,7 @@ async def test_round_robin_allocation(): pool_worker_ids=["pool"], allocation_method=RunAllocationMethod.ROUND_ROBIN, func_import_path_or_key="add", - send_channel=False, + func_args=(i, 1), func_kwargs={}, ) @@ -337,7 +337,7 @@ async def test_random_allocation(): pool_worker_ids=["pool"], allocation_method=RunAllocationMethod.RANDOM, func_import_path_or_key="add", - send_channel=False, + func_args=(i, 1), func_kwargs={}, ) @@ -368,7 +368,7 @@ async def test_allocation_with_specific_workers(): pool_worker_ids=[("pool", 0), ("pool", 2)], allocation_method=RunAllocationMethod.ROUND_ROBIN, func_import_path_or_key="add", - send_channel=False, + func_args=(i, 1), func_kwargs={}, ) @@ -397,7 +397,7 @@ async def test_empty_workers_raises(): pool_worker_ids=[], allocation_method=RunAllocationMethod.ROUND_ROBIN, func_import_path_or_key="add", - send_channel=False, + func_args=(1, 2), func_kwargs={}, ) @@ -446,7 +446,7 @@ async def test_multiple_pools(): pool_id="fast", worker_id=0, func_import_path_or_key="add", - send_channel=False, + func_args=(5, 3), func_kwargs={}, ) @@ -456,7 +456,7 @@ async def test_multiple_pools(): pool_id="slow", worker_id=0, func_import_path_or_key="multiply", - send_channel=False, + func_args=(4, 7), func_kwargs={}, ) @@ -492,7 +492,7 @@ async def test_concurrent_jobs(): pool_worker_ids=["pool"], allocation_method=RunAllocationMethod.ROUND_ROBIN, func_import_path_or_key="add", - send_channel=False, + func_args=(i, i), func_kwargs={}, ) @@ -527,7 +527,7 @@ async def test_async_function(): pool_id="pool", worker_id=0, func_import_path_or_key="async_add", - send_channel=False, + func_args=(10, 20), func_kwargs={}, ) @@ -594,7 +594,7 @@ async def test_flush_pool_stdout(): pool_id="mp_pool", worker_id=0, func_import_path_or_key="mp_print", - send_channel=False, + func_args=("hello",), func_kwargs={}, ) @@ -650,7 +650,7 @@ async def test_flush_all_pool_stdout(): pool_id="mp_pool", worker_id=0, func_import_path_or_key="mp_print", - send_channel=False, + func_args=("proc0",), func_kwargs={}, ) @@ -658,7 +658,7 @@ async def test_flush_all_pool_stdout(): pool_id="mp_pool", worker_id=1, func_import_path_or_key="mp_print", - send_channel=False, + func_args=("proc1",), func_kwargs={}, ) diff --git a/netrun/pts/tests/05_execution_manager/test_execution_manager_remote.pct.py b/netrun/pts/tests/05_execution_manager/test_execution_manager_remote.pct.py index 40c4fa69..dd0edd31 100644 --- a/netrun/pts/tests/05_execution_manager/test_execution_manager_remote.pct.py +++ b/netrun/pts/tests/05_execution_manager/test_execution_manager_remote.pct.py @@ -172,13 +172,9 @@ async def test_remote_pool_run_function(): await client.send( worker_id=0, key=ExecutionManagerProtocolKeys.RUN.value, - data=("msg_2", "add", "run_1", False, (3, 4), {}) + data=("msg_2", "add", "run_1", (3, 4), {}) ) - # Get RUN_STARTED - msg = await client.recv(timeout=10.0) - assert msg.key == ExecutionManagerProtocolKeys.UP_RUN_STARTED.value - # Get result msg = await client.recv(timeout=10.0) assert msg.key == ExecutionManagerProtocolKeys.UP_RUN_RESPONSE.value @@ -214,22 +210,18 @@ async def test_remote_pool_multiple_workers(): await client.send( worker_id=worker_id, key=ExecutionManagerProtocolKeys.RUN.value, - data=(f"msg_run_{worker_id}", "multiply", f"run_{worker_id}", False, (worker_id + 1, 10), {}) + data=(f"msg_run_{worker_id}", "multiply", f"run_{worker_id}", (worker_id + 1, 10), {}) ) - # Collect results (2 RUN_STARTED + 2 RUN_RESPONSE) - started_count = 0 + # Collect results (2 RUN_RESPONSE) results = {} - for _ in range(4): + for _ in range(2): msg = await client.recv(timeout=10.0) - if msg.key == ExecutionManagerProtocolKeys.UP_RUN_STARTED.value: - started_count += 1 - elif msg.key == ExecutionManagerProtocolKeys.UP_RUN_RESPONSE.value: - msg_id = msg.data[0] - result = msg.data[4] - results[msg_id] = result - - assert started_count == 2 + assert msg.key == ExecutionManagerProtocolKeys.UP_RUN_RESPONSE.value + msg_id = msg.data[0] + result = msg.data[4] + results[msg_id] = result + assert results["msg_run_0"] == 10 # 1 * 10 assert results["msg_run_1"] == 20 # 2 * 10 @@ -257,13 +249,9 @@ async def test_remote_pool_function_with_kwargs(): await client.send( worker_id=0, key=ExecutionManagerProtocolKeys.RUN.value, - data=("msg_2", "kwargs_fn", "run_1", False, (5,), {"b": 20, "c": 200}) + data=("msg_2", "kwargs_fn", "run_1", (5,), {"b": 20, "c": 200}) ) - # Get RUN_STARTED - msg = await client.recv(timeout=10.0) - assert msg.key == ExecutionManagerProtocolKeys.UP_RUN_STARTED.value - # Get result msg = await client.recv(timeout=10.0) assert msg.key == ExecutionManagerProtocolKeys.UP_RUN_RESPONSE.value @@ -297,22 +285,18 @@ async def test_remote_pool_concurrent_runs(): await client.send( worker_id=i, key=ExecutionManagerProtocolKeys.RUN.value, - data=(f"msg_run_{i}", "add", f"run_{i}", False, (i, i), {}) + data=(f"msg_run_{i}", "add", f"run_{i}", (i, i), {}) ) - # Collect all results (4 RUN_STARTED + 4 RUN_RESPONSE) - started_count = 0 + # Collect all results (4 RUN_RESPONSE) results = {} - for _ in range(8): + for _ in range(4): msg = await client.recv(timeout=10.0) - if msg.key == ExecutionManagerProtocolKeys.UP_RUN_STARTED.value: - started_count += 1 - elif msg.key == ExecutionManagerProtocolKeys.UP_RUN_RESPONSE.value: - msg_id = msg.data[0] - result = msg.data[4] - results[msg_id] = result - - assert started_count == 4 + assert msg.key == ExecutionManagerProtocolKeys.UP_RUN_RESPONSE.value + msg_id = msg.data[0] + result = msg.data[4] + results[msg_id] = result + # Verify results for i in range(4): assert results[f"msg_run_{i}"] == i + i @@ -339,7 +323,7 @@ async def test_execution_manager_with_remote_pool(): pool_id="remote", worker_id=0, func_import_path_or_key="add", - send_channel=False, + func_args=(3, 4), func_kwargs={}, ) @@ -366,7 +350,7 @@ async def test_execution_manager_remote_multiple_workers(): pool_id="remote", worker_id=worker_id, func_import_path_or_key="multiply", - send_channel=False, + func_args=(worker_id + 1, 10), func_kwargs={}, ) @@ -402,7 +386,7 @@ async def test_execution_manager_remote_kwargs(): pool_id="remote", worker_id=0, func_import_path_or_key="kwargs_fn", - send_channel=False, + func_args=(5,), func_kwargs={"b": 20, "c": 200}, ) @@ -424,7 +408,7 @@ async def test_execution_manager_remote_timestamps(): pool_id="remote", worker_id=0, func_import_path_or_key="slow", - send_channel=False, + func_args=(0.05,), func_kwargs={}, ) diff --git a/netrun/pts/tests/05_execution_manager/test_execution_manager_thread.pct.py b/netrun/pts/tests/05_execution_manager/test_execution_manager_thread.pct.py index 968de41e..f8dbe23f 100644 --- a/netrun/pts/tests/05_execution_manager/test_execution_manager_thread.pct.py +++ b/netrun/pts/tests/05_execution_manager/test_execution_manager_thread.pct.py @@ -219,7 +219,7 @@ async def test_send_function_and_run(): pool_id="pool", worker_id=0, func_import_path_or_key="add", - send_channel=False, + func_args=(3, 4), func_kwargs={}, ) @@ -252,7 +252,7 @@ async def test_send_function_to_pool(): pool_id="pool", worker_id=worker_id, func_import_path_or_key="multiply", - send_channel=False, + func_args=(worker_id + 1, 10), func_kwargs={}, ) @@ -283,7 +283,7 @@ async def test_job_result_timestamps(): pool_id="pool", worker_id=0, func_import_path_or_key="slow", - send_channel=False, + func_args=(0.1,), func_kwargs={}, ) @@ -315,7 +315,7 @@ async def test_non_serializable_result_for_main_process(): pool_id="pool", worker_id=0, func_import_path_or_key="nonserialized", - send_channel=False, + func_args=(), func_kwargs={}, ) @@ -346,7 +346,7 @@ async def test_function_with_kwargs(): pool_id="pool", worker_id=0, func_import_path_or_key="kwargs_fn", - send_channel=False, + func_args=(1,), func_kwargs={}, ) @@ -357,7 +357,7 @@ async def test_function_with_kwargs(): pool_id="pool", worker_id=0, func_import_path_or_key="kwargs_fn", - send_channel=False, + func_args=(5,), func_kwargs={"b": 20, "c": 200}, ) @@ -388,7 +388,7 @@ async def test_round_robin_allocation(): pool_worker_ids=["pool"], allocation_method=RunAllocationMethod.ROUND_ROBIN, func_import_path_or_key="add", - send_channel=False, + func_args=(i, 1), func_kwargs={}, ) @@ -419,7 +419,7 @@ async def test_random_allocation(): pool_worker_ids=["pool"], allocation_method=RunAllocationMethod.RANDOM, func_import_path_or_key="add", - send_channel=False, + func_args=(i, 1), func_kwargs={}, ) @@ -450,7 +450,7 @@ async def test_allocation_with_specific_workers(): pool_worker_ids=[("pool", 0), ("pool", 2)], allocation_method=RunAllocationMethod.ROUND_ROBIN, func_import_path_or_key="add", - send_channel=False, + func_args=(i, 1), func_kwargs={}, ) @@ -479,7 +479,7 @@ async def test_empty_workers_raises(): pool_worker_ids=[], allocation_method=RunAllocationMethod.ROUND_ROBIN, func_import_path_or_key="add", - send_channel=False, + func_args=(1, 2), func_kwargs={}, ) @@ -528,7 +528,7 @@ async def test_multiple_pools(): pool_id="fast", worker_id=0, func_import_path_or_key="add", - send_channel=False, + func_args=(5, 3), func_kwargs={}, ) @@ -538,7 +538,7 @@ async def test_multiple_pools(): pool_id="slow", worker_id=0, func_import_path_or_key="multiply", - send_channel=False, + func_args=(4, 7), func_kwargs={}, ) @@ -574,7 +574,7 @@ async def test_concurrent_jobs(): pool_worker_ids=["pool"], allocation_method=RunAllocationMethod.ROUND_ROBIN, func_import_path_or_key="add", - send_channel=False, + func_args=(i, i), func_kwargs={}, ) @@ -609,7 +609,7 @@ async def test_async_function(): pool_id="pool", worker_id=0, func_import_path_or_key="async_add", - send_channel=False, + func_args=(10, 20), func_kwargs={}, ) @@ -638,7 +638,7 @@ async def test_main_pool(): pool_id="main", worker_id=0, func_import_path_or_key="add", - send_channel=False, + func_args=(100, 200), func_kwargs={}, ) @@ -673,7 +673,7 @@ async def test_worker_jobs_cleanup_on_cancellation(): pool_id="pool", worker_id=0, func_import_path_or_key="slow", - send_channel=False, + func_args=(30.0,), # 30s — will be cancelled long before func_kwargs={}, ), @@ -697,7 +697,7 @@ async def test_worker_jobs_cleanup_on_cancellation(): pool_worker_ids=["pool"], allocation_method=RunAllocationMethod.LEAST_BUSY, func_import_path_or_key="add", - send_channel=False, + func_args=(1, 2), func_kwargs={}, ) @@ -732,7 +732,7 @@ async def test_worker_exception_does_not_crash_loop(): pool_id="pool", worker_id=0, func_import_path_or_key="error_fn", - send_channel=False, + func_args=(), func_kwargs={}, ), @@ -744,7 +744,7 @@ async def test_worker_exception_does_not_crash_loop(): pool_id="pool", worker_id=0, func_import_path_or_key="add", - send_channel=False, + func_args=(1, 2), func_kwargs={}, ) @@ -777,7 +777,7 @@ async def test_async_subprocess_in_thread_pool(): pool_id="pool", worker_id=0, func_import_path_or_key="subprocess_fn", - send_channel=False, + func_args=(), func_kwargs={}, ), @@ -807,7 +807,7 @@ async def test_nested_async_in_thread_pool(): pool_id="pool", worker_id=0, func_import_path_or_key="nested", - send_channel=False, + func_args=(), func_kwargs={}, ) @@ -848,7 +848,7 @@ def cpu_bound_sync(n: int) -> int: pool_id="pool", worker_id=i, func_import_path_or_key="cpu", - send_channel=False, + func_args=(100_000,), func_kwargs={}, ) @@ -899,7 +899,7 @@ async def waiter() -> str: pool_id="pool", worker_id=0, func_import_path_or_key="setter", - send_channel=False, + func_args=(0.1,), func_kwargs={}, ) @@ -909,7 +909,7 @@ async def waiter() -> str: pool_id="pool", worker_id=1, func_import_path_or_key="waiter", - send_channel=False, + func_args=(), func_kwargs={}, ) @@ -982,7 +982,7 @@ def crash_worker_func(a: int, b: int) -> int: pool_id="pool", worker_id=0, func_import_path_or_key="crash", - send_channel=False, + func_args=(1, 2), func_kwargs={}, ), @@ -995,7 +995,7 @@ def crash_worker_func(a: int, b: int) -> int: pool_id="pool", worker_id=1, func_import_path_or_key="add", - send_channel=False, + func_args=(10, 20), func_kwargs={}, ), diff --git a/netrun/pts/tests/06_net/test_net.pct.py b/netrun/pts/tests/06_net/test_net.pct.py index 80843ba2..417de9e8 100644 --- a/netrun/pts/tests/06_net/test_net.pct.py +++ b/netrun/pts/tests/06_net/test_net.pct.py @@ -25,7 +25,6 @@ from netrun.net._net import ( Net, - NetProtocolKeys, NodeExecutionContext, NodeExecutionResult, NodeFailureContext, @@ -60,37 +59,6 @@ from netrun.execution_manager import RunAllocationMethod from netrun.packets import LazyPacketValueSpec -# %% [markdown] -# ## NetProtocolKeys Tests - -# %% -#|export -def test_net_protocol_keys_values(): - """Test that NetProtocolKeys have expected string values.""" - assert NetProtocolKeys.UP_CREATE_PACKET.value == "net:create-packet" - assert NetProtocolKeys.UP_CREATE_PACKET_RESPONSE.value == "net:create-packet-response" - assert NetProtocolKeys.UP_CONSUME_PACKET.value == "net:consume-packet" - assert NetProtocolKeys.UP_CONSUME_PACKET_RESPONSE.value == "net:consume-packet-response" - assert NetProtocolKeys.UP_LOAD_OUTPUT_PORT.value == "net:load-output-port" - assert NetProtocolKeys.UP_LOAD_OUTPUT_PORT_RESPONSE.value == "net:load-output-port-response" - assert NetProtocolKeys.UP_SEND_OUTPUT_SALVO.value == "net:send-salvo" - assert NetProtocolKeys.UP_SEND_OUTPUT_SALVO_RESPONSE.value == "net:send-salvo-response" - assert NetProtocolKeys.UP_CANCEL_EPOCH.value == "net:cancel-epoch" - assert NetProtocolKeys.UP_PRINT_BUFFER.value == "net:print-buffer" - -# %% -test_net_protocol_keys_values() - -# %% -#|export -def test_net_protocol_keys_uniqueness(): - """Test that all protocol keys have unique values.""" - values = [key.value for key in NetProtocolKeys] - assert len(values) == len(set(values)), "Protocol keys must have unique values" - -# %% -test_net_protocol_keys_uniqueness() - # %% [markdown] # ## NodeExecutionContext Tests diff --git a/netrun/src/netrun/execution_manager.py b/netrun/src/netrun/execution_manager.py index 3f0114e3..b54161ff 100644 --- a/netrun/src/netrun/execution_manager.py +++ b/netrun/src/netrun/execution_manager.py @@ -17,7 +17,6 @@ import random import functools -from .rpc.base import RPCChannel from .pool.base import POOL_UP_ERROR_KEYS, _check_error_and_raise from .pool.thread import ThreadPool from .pool.multiprocess import MultiprocessPool @@ -26,28 +25,18 @@ # %% pts/netrun/05_execution_manager.pct.py 5 async def _async_func_runner( - channel: RPCChannel, func: Callable[..., Awaitable[Any]], - send_channel: bool, args: tuple, kwargs: dict, ) -> Any: if asyncio.iscoroutinefunction(func): - if send_channel: - return await func(channel, *args, **kwargs) - else: - return await func(*args, **kwargs) + return await func(*args, **kwargs) else: - if send_channel: - return func(channel, *args, **kwargs) - else: - return func(*args, **kwargs) + return func(*args, **kwargs) # %% pts/netrun/05_execution_manager.pct.py 6 def _func_runner( - channel: RPCChannel, func: Callable[..., Any], - send_channel: bool, args: tuple, kwargs: dict, shared_loop: asyncio.AbstractEventLoop | None = None, @@ -58,13 +47,8 @@ def _func_runner( async dispatch internally. When called directly without a preprocessor, async functions are submitted to the shared event loop. """ - if send_channel: - call_args = (channel, *args) - else: - call_args = args - if asyncio.iscoroutinefunction(func): - coro = func(*call_args, **kwargs) + coro = func(*args, **kwargs) if shared_loop is not None: future = asyncio.run_coroutine_threadsafe(coro, shared_loop) return future.result() @@ -76,7 +60,7 @@ def _func_runner( finally: loop.close() else: - return func(*call_args, **kwargs) + return func(*args, **kwargs) # %% pts/netrun/05_execution_manager.pct.py 8 _worker_state = threading.local() @@ -106,19 +90,13 @@ class ExecutionManagerProtocolKeys(Enum): RUN = "exec-manager:run" """ Run a function. - Args: msg_id, func_import_path_or_key, run_id, send_channel, args, kwargs - """ - - UP_RUN_STARTED = "exec-manager-up:run-started" - """ - Notification from the worker that a run has been submitted and started. - Args: msg_id, timestamp_utc_started + Args: msg_id, func_import_path_or_key, run_id, args, kwargs """ UP_RUN_RESPONSE = "exec-manager-up:run-response" """ Response to RUN from the worker. - Args: msg_id, converted_to_str, _res + Args: msg_id, timestamp_utc_started, timestamp_utc_completed, converted_to_str, _res """ SEND_FUNCTION = "exec-manager:send-function" @@ -166,9 +144,8 @@ def _worker_func( key, data = channel.recv() # RUN if key == ExecutionManagerProtocolKeys.RUN.value: - msg_id, func_import_path_or_key, run_id, send_channel, args, kwargs = data + msg_id, func_import_path_or_key, run_id, args, kwargs = data timestamp_utc_started = None - started_sent = False try: if func_import_path_or_key in registered_functions: func = registered_functions[func_import_path_or_key] @@ -181,12 +158,8 @@ def _worker_func( func = func_preprocessor.call_for_sync_worker(func) timestamp_utc_started = get_timestamp_utc() - channel.send(ExecutionManagerProtocolKeys.UP_RUN_STARTED.value, (msg_id, timestamp_utc_started)) - started_sent = True res = _func_runner( - channel=channel, func=func, - send_channel=send_channel, args=args, kwargs=kwargs, shared_loop=shared_loop, @@ -194,10 +167,7 @@ def _worker_func( # Call done callback if provided if func_done_callback is not None: - if send_channel: - func_done_callback(channel, *args, **kwargs, result=res) - else: - func_done_callback(*args, **kwargs, result=res) + func_done_callback(*args, **kwargs, result=res) timestamp_utc_completed = get_timestamp_utc() if is_in_main_process: @@ -212,8 +182,6 @@ def _worker_func( if timestamp_utc_started is None: timestamp_utc_started = timestamp_utc_error try: - if not started_sent: - channel.send(ExecutionManagerProtocolKeys.UP_RUN_STARTED.value, (msg_id, timestamp_utc_started)) channel.send(ExecutionManagerProtocolKeys.UP_RUN_RESPONSE.value, (msg_id, timestamp_utc_started, timestamp_utc_error, False, e)) except Exception: pass # Worker loop must survive @@ -246,26 +214,19 @@ async def _async_worker_func( """ registered_functions: dict[str, Callable[..., Awaitable] | Callable[..., None]] = {} - async def _handle_run(msg_id, func, send_channel, args, kwargs): + async def _handle_run(msg_id, func, args, kwargs): """Execute a single RUN request and send the response.""" timestamp_utc_started = get_timestamp_utc() try: - await channel.send(ExecutionManagerProtocolKeys.UP_RUN_STARTED.value, (msg_id, timestamp_utc_started)) - res = await _async_func_runner( - channel=channel, func=func, - send_channel=send_channel, args=args, kwargs=kwargs, ) # Call done callback if provided if func_done_callback is not None: - if send_channel: - callback_result = func_done_callback(channel, *args, **kwargs, result=res) - else: - callback_result = func_done_callback(*args, **kwargs, result=res) + callback_result = func_done_callback(*args, **kwargs, result=res) # Await if the callback is async if asyncio.iscoroutine(callback_result): await callback_result @@ -289,7 +250,7 @@ async def _handle_run(msg_id, func, send_channel, args, kwargs): key, data = await channel.recv() # RUN if key == ExecutionManagerProtocolKeys.RUN.value: - msg_id, func_import_path_or_key, run_id, send_channel, args, kwargs = data + msg_id, func_import_path_or_key, run_id, args, kwargs = data if func_import_path_or_key in registered_functions: func = registered_functions[func_import_path_or_key] else: @@ -301,7 +262,7 @@ async def _handle_run(msg_id, func, send_channel, args, kwargs): func = func_preprocessor(func) # Spawn as concurrent task instead of awaiting inline - task = asyncio.create_task(_handle_run(msg_id, func, send_channel, args, kwargs)) + task = asyncio.create_task(_handle_run(msg_id, func, args, kwargs)) pending_tasks.add(task) task.add_done_callback(pending_tasks.discard) # SEND_FUNCTION @@ -627,7 +588,6 @@ async def run( pool_id: str, worker_id: str, func_import_path_or_key: str, - send_channel: bool, func_args, func_kwargs, ) -> JobResult: @@ -638,7 +598,6 @@ async def run( pool_id: The ID of the pool to run the function in. worker_id: The ID of the worker to run the function on. func_import_path_or_key: The import path or key of the function to run (for the latter, use send_function to register the function first) - send_channel: Whether to send the worker RPC channel to the function. func_args: The arguments to pass to the function. func_kwargs: The keyword arguments to pass to the function. @@ -668,25 +627,22 @@ async def run( pool_id=pool_id, worker_id=worker_id, key=ExecutionManagerProtocolKeys.RUN.value, - data=(func_import_path_or_key, run_id, send_channel, func_args, func_kwargs), + data=(func_import_path_or_key, run_id, func_args, func_kwargs), ) - # Wait for UP_RUN_STARTED - started_msg = await self._recv_msg(pool_id, msg_id, expect=ExecutionManagerProtocolKeys.UP_RUN_STARTED, close_msg_queue=False) - job_info.timestamp_utc_started = started_msg.data[0] # timestamp_utc_started - - # Wait for UP_RUN_RESPONSE + # Wait for UP_RUN_RESPONSE (single response containing all timestamps) msg = await self._recv_msg(pool_id, msg_id, expect=ExecutionManagerProtocolKeys.UP_RUN_RESPONSE, close_msg_queue=True) msg_id = None # Already cleaned up by close_msg_queue=True self._worker_jobs[(pool_id, worker_id)].remove(job_info) timestamp_utc_started, timestamp_utc_completed, converted_to_str, _res = msg.data + job_info.timestamp_utc_started = timestamp_utc_started if isinstance(_res, Exception): raise _res return JobResult( timestamp_utc_submitted=job_info.timestamp_utc_submitted, - timestamp_utc_started=job_info.timestamp_utc_started, + timestamp_utc_started=timestamp_utc_started, timestamp_utc_completed=timestamp_utc_completed, func_import_path_or_key=job_info.func_import_path_or_key, pool_id=job_info.pool_id, @@ -709,7 +665,6 @@ async def run_allocate( pool_worker_ids: list[str | tuple[str, str]], allocation_method: RunAllocationMethod, func_import_path_or_key: str, - send_channel: bool, func_args, func_kwargs, ) -> JobResult: @@ -755,7 +710,6 @@ def active_job_count(pool_worker: tuple[str, str]) -> int: pool_id=pool_id, worker_id=worker_id, func_import_path_or_key=func_import_path_or_key, - send_channel=send_channel, func_args=func_args, func_kwargs=func_kwargs, ) diff --git a/netrun/src/netrun/net/_net/__init__.py b/netrun/src/netrun/net/_net/__init__.py index 865b7a0d..efd0b1b3 100644 --- a/netrun/src/netrun/net/_net/__init__.py +++ b/netrun/src/netrun/net/_net/__init__.py @@ -1,6 +1,5 @@ # Re-export from _context from ._context import ( - NetProtocolKeys, EpochCancelled, MaxEpochsExceeded, PacketTypeMismatch, diff --git a/netrun/src/netrun/net/_net/_context.py b/netrun/src/netrun/net/_net/_context.py index 2ab3e94d..20435141 100644 --- a/netrun/src/netrun/net/_net/_context.py +++ b/netrun/src/netrun/net/_net/_context.py @@ -1,6 +1,6 @@ # AUTOGENERATED! DO NOT EDIT! File to edit: pts/netrun/06_net/01_net/00_context.pct.py -__all__ = ['ConsumedOutputPacket', 'DeferredActionQueue', 'EpochCancelled', 'EpochError', 'EpochLog', 'EpochRecord', 'MaxEpochsExceeded', 'NetActionLog', 'NetEventLog', 'NetFuncPreprocessor', 'NetFuncPreprocessorNodeConfig', 'NetProtocolKeys', 'NodeExecutionContext', 'NodeExecutionResult', 'NodeFailureContext', 'NodeLogEntry', 'PacketTypeMismatch', 'create_net_func_preprocessor'] +__all__ = ['ConsumedOutputPacket', 'DeferredActionQueue', 'EpochCancelled', 'EpochError', 'EpochLog', 'EpochRecord', 'MaxEpochsExceeded', 'NetActionLog', 'NetEventLog', 'NetFuncPreprocessor', 'NetFuncPreprocessorNodeConfig', 'NodeExecutionContext', 'NodeExecutionResult', 'NodeFailureContext', 'NodeLogEntry', 'PacketTypeMismatch', 'create_net_func_preprocessor'] # %% pts/netrun/06_net/01_net/00_context.pct.py 3 import asyncio @@ -8,7 +8,6 @@ import uuid from dataclasses import dataclass, field from datetime import datetime, timezone -from enum import Enum from typing import Any, NoReturn, get_origin from collections.abc import Callable import importlib @@ -22,41 +21,6 @@ from ...execution_manager import _worker_state # %% pts/netrun/06_net/01_net/00_context.pct.py 5 -class NetProtocolKeys(Enum): - """Protocol keys for Net <-> Worker communication.""" - - # Upstream (Worker -> Net) - sent via channel when send_channel=True - UP_CREATE_PACKET = "net:create-packet" - """Create a new packet. Args: (epoch_id, value_or_lazy)""" - - UP_CREATE_PACKET_RESPONSE = "net:create-packet-response" - """Response with packet ID. Args: (packet_id,)""" - - UP_CONSUME_PACKET = "net:consume-packet" - """Consume a packet. Args: (epoch_id, packet_id)""" - - UP_CONSUME_PACKET_RESPONSE = "net:consume-packet-response" - """Response with packet value. Args: (value,)""" - - UP_LOAD_OUTPUT_PORT = "net:load-output-port" - """Load packet into output port. Args: (epoch_id, port_name, packet_id)""" - - UP_LOAD_OUTPUT_PORT_RESPONSE = "net:load-output-port-response" - """Acknowledgement. Args: ()""" - - UP_SEND_OUTPUT_SALVO = "net:send-salvo" - """Send output salvo. Args: (epoch_id, salvo_condition_name)""" - - UP_SEND_OUTPUT_SALVO_RESPONSE = "net:send-salvo-response" - """Acknowledgement. Args: ()""" - - UP_CANCEL_EPOCH = "net:cancel-epoch" - """Cancel the current epoch. Args: (epoch_id,)""" - - UP_PRINT_BUFFER = "net:print-buffer" - """Send captured print output. Args: (epoch_id, buffer: list[str])""" - -# %% pts/netrun/06_net/01_net/00_context.pct.py 7 @dataclass class NodeLogEntry: """A structured log entry from ctx.log() inside a node function.""" @@ -262,7 +226,7 @@ def from_dict(cls, d: dict[str, Any]) -> "EpochLog": ], ) -# %% pts/netrun/06_net/01_net/00_context.pct.py 9 +# %% pts/netrun/06_net/01_net/00_context.pct.py 7 def _format_value(v: Any) -> str: """Format a value for log output.""" if isinstance(v, str): @@ -294,7 +258,7 @@ def _format_epoch_log(epoch_log: "EpochLog") -> str: parts.append(f"{k}={_format_value(v)}") return " ".join(parts) -# %% pts/netrun/06_net/01_net/00_context.pct.py 11 +# %% pts/netrun/06_net/01_net/00_context.pct.py 9 class EpochCancelled(Exception): """Raised when an epoch is cancelled via ctx.cancel_epoch().""" pass @@ -774,7 +738,7 @@ def _get_execution_result(self) -> "NodeExecutionResult": structured_log_buffer=self._structured_log_buffer.copy(), ) -# %% pts/netrun/06_net/01_net/00_context.pct.py 13 +# %% pts/netrun/06_net/01_net/00_context.pct.py 11 @dataclass class NodeExecutionResult: """Result of a node function execution. @@ -806,7 +770,7 @@ class NodeExecutionResult: exception: Exception | None = None """Exception raised by the node function (if any).""" -# %% pts/netrun/06_net/01_net/00_context.pct.py 15 +# %% pts/netrun/06_net/01_net/00_context.pct.py 13 @dataclass class NodeFailureContext: """Context object passed to on_node_failure callbacks.""" @@ -836,7 +800,7 @@ def print(self, *args, sep: str = " ", end: str = "\n") -> None: message = sep.join(str(arg) for arg in args) + end self._print_buffer.append((timestamp, message)) -# %% pts/netrun/06_net/01_net/00_context.pct.py 17 +# %% pts/netrun/06_net/01_net/00_context.pct.py 15 @dataclass class ConsumedOutputPacket: """A packet retrieved from an output queue. @@ -867,7 +831,7 @@ class ConsumedOutputPacket: epoch_id: str """The epoch that produced this packet.""" -# %% pts/netrun/06_net/01_net/00_context.pct.py 19 +# %% pts/netrun/06_net/01_net/00_context.pct.py 17 @dataclass class _EpochState: """Internal mutable state for an epoch's lifecycle.""" @@ -1017,7 +981,7 @@ def to_log( # Backward compat alias EpochRecord = _EpochState -# %% pts/netrun/06_net/01_net/00_context.pct.py 21 +# %% pts/netrun/06_net/01_net/00_context.pct.py 19 @dataclass class DeferredActionQueue: """Queue of net actions to be committed on success or discarded on failure.""" @@ -1051,7 +1015,7 @@ def discard(self) -> None: self.packet_values.clear() self.deferred_to_real_ids.clear() -# %% pts/netrun/06_net/01_net/00_context.pct.py 23 +# %% pts/netrun/06_net/01_net/00_context.pct.py 21 @dataclass class NetFuncPreprocessorNodeConfig: """Picklable configuration for a node's preprocessor behavior. diff --git a/netrun/src/netrun/net/_net/_net.py b/netrun/src/netrun/net/_net/_net.py index b6019a3b..d4666230 100644 --- a/netrun/src/netrun/net/_net/_net.py +++ b/netrun/src/netrun/net/_net/_net.py @@ -2404,7 +2404,6 @@ async def _execute_epoch_with_retry( pool_worker_ids=config.pools, allocation_method=allocation_method, func_import_path_or_key=func_key, - send_channel=False, # Deferred mode doesn't need channel func_args=(epoch_id, node_name, packets, packet_values), func_kwargs={ "retry_count": retry_count, diff --git a/netrun/src/tests/execution_manager/test_execution_manager_async.py b/netrun/src/tests/execution_manager/test_execution_manager_async.py index 093a3a97..c2ae27ce 100644 --- a/netrun/src/tests/execution_manager/test_execution_manager_async.py +++ b/netrun/src/tests/execution_manager/test_execution_manager_async.py @@ -121,7 +121,7 @@ async def test_send_function_and_run(): pool_id="pool", worker_id=0, func_import_path_or_key="add", - send_channel=False, + func_args=(3, 4), func_kwargs={}, ) @@ -148,7 +148,7 @@ async def test_send_function_to_pool(): pool_id="pool", worker_id=0, func_import_path_or_key="multiply", - send_channel=False, + func_args=(5, 10), func_kwargs={}, ) @@ -170,7 +170,7 @@ async def test_job_result_timestamps(): pool_id="pool", worker_id=0, func_import_path_or_key="slow", - send_channel=False, + func_args=(0.1,), func_kwargs={}, ) @@ -197,7 +197,7 @@ async def test_non_serializable_result(): pool_id="pool", worker_id=0, func_import_path_or_key="nonserialized", - send_channel=False, + func_args=(), func_kwargs={}, ) @@ -224,7 +224,7 @@ async def test_function_with_kwargs(): pool_id="pool", worker_id=0, func_import_path_or_key="kwargs_fn", - send_channel=False, + func_args=(1,), func_kwargs={}, ) @@ -235,7 +235,7 @@ async def test_function_with_kwargs(): pool_id="pool", worker_id=0, func_import_path_or_key="kwargs_fn", - send_channel=False, + func_args=(5,), func_kwargs={"b": 20, "c": 200}, ) @@ -259,7 +259,7 @@ async def test_round_robin_allocation(): pool_worker_ids=["pool"], allocation_method=RunAllocationMethod.ROUND_ROBIN, func_import_path_or_key="add", - send_channel=False, + func_args=(i, 1), func_kwargs={}, ) @@ -284,7 +284,7 @@ async def test_empty_workers_raises(): pool_worker_ids=[], allocation_method=RunAllocationMethod.ROUND_ROBIN, func_import_path_or_key="add", - send_channel=False, + func_args=(1, 2), func_kwargs={}, ) @@ -319,7 +319,7 @@ async def test_multiple_pools(): pool_id="pool_a", worker_id=0, func_import_path_or_key="add", - send_channel=False, + func_args=(5, 3), func_kwargs={}, ) @@ -329,7 +329,7 @@ async def test_multiple_pools(): pool_id="pool_b", worker_id=0, func_import_path_or_key="multiply", - send_channel=False, + func_args=(4, 7), func_kwargs={}, ) @@ -354,7 +354,7 @@ async def test_async_function(): pool_id="pool", worker_id=0, func_import_path_or_key="async_add", - send_channel=False, + func_args=(10, 20), func_kwargs={}, ) @@ -376,7 +376,7 @@ async def test_pool_class_directly(): pool_id="pool", worker_id=0, func_import_path_or_key="add", - send_channel=False, + func_args=(100, 200), func_kwargs={}, ) @@ -415,7 +415,7 @@ async def slow_async_func(delay: float) -> str: pool_id="pool", worker_id=0, func_import_path_or_key="slow", - send_channel=False, + func_args=(0.2,), func_kwargs={}, ) @@ -456,7 +456,7 @@ async def test_worker_exception_does_not_hang(): pool_id="pool", worker_id=0, func_import_path_or_key="error_fn", - send_channel=False, + func_args=(), func_kwargs={}, ), @@ -469,7 +469,7 @@ async def test_worker_exception_does_not_hang(): pool_id="pool", worker_id=0, func_import_path_or_key="add", - send_channel=False, + func_args=(10, 20), func_kwargs={}, ) diff --git a/netrun/src/tests/execution_manager/test_execution_manager_multiprocess.py b/netrun/src/tests/execution_manager/test_execution_manager_multiprocess.py index 93f270e8..b1421e31 100644 --- a/netrun/src/tests/execution_manager/test_execution_manager_multiprocess.py +++ b/netrun/src/tests/execution_manager/test_execution_manager_multiprocess.py @@ -122,7 +122,7 @@ async def test_send_function_and_run(): pool_id="pool", worker_id=0, func_import_path_or_key="add", - send_channel=False, + func_args=(3, 4), func_kwargs={}, ) @@ -150,7 +150,7 @@ async def test_send_function_to_pool(): pool_id="pool", worker_id=worker_id, func_import_path_or_key="multiply", - send_channel=False, + func_args=(worker_id + 1, 10), func_kwargs={}, ) @@ -173,7 +173,7 @@ async def test_job_result_timestamps(): pool_id="pool", worker_id=0, func_import_path_or_key="slow", - send_channel=False, + func_args=(0.1,), func_kwargs={}, ) @@ -201,7 +201,7 @@ async def test_function_with_kwargs(): pool_id="pool", worker_id=0, func_import_path_or_key="kwargs_fn", - send_channel=False, + func_args=(1,), func_kwargs={}, ) @@ -212,7 +212,7 @@ async def test_function_with_kwargs(): pool_id="pool", worker_id=0, func_import_path_or_key="kwargs_fn", - send_channel=False, + func_args=(5,), func_kwargs={"b": 20, "c": 200}, ) @@ -236,7 +236,7 @@ async def test_round_robin_allocation(): pool_worker_ids=["pool"], allocation_method=RunAllocationMethod.ROUND_ROBIN, func_import_path_or_key="add", - send_channel=False, + func_args=(i, 1), func_kwargs={}, ) @@ -263,7 +263,7 @@ async def test_random_allocation(): pool_worker_ids=["pool"], allocation_method=RunAllocationMethod.RANDOM, func_import_path_or_key="add", - send_channel=False, + func_args=(i, 1), func_kwargs={}, ) @@ -290,7 +290,7 @@ async def test_allocation_with_specific_workers(): pool_worker_ids=[("pool", 0), ("pool", 2)], allocation_method=RunAllocationMethod.ROUND_ROBIN, func_import_path_or_key="add", - send_channel=False, + func_args=(i, 1), func_kwargs={}, ) @@ -315,7 +315,7 @@ async def test_empty_workers_raises(): pool_worker_ids=[], allocation_method=RunAllocationMethod.ROUND_ROBIN, func_import_path_or_key="add", - send_channel=False, + func_args=(1, 2), func_kwargs={}, ) @@ -350,7 +350,7 @@ async def test_multiple_pools(): pool_id="fast", worker_id=0, func_import_path_or_key="add", - send_channel=False, + func_args=(5, 3), func_kwargs={}, ) @@ -360,7 +360,7 @@ async def test_multiple_pools(): pool_id="slow", worker_id=0, func_import_path_or_key="multiply", - send_channel=False, + func_args=(4, 7), func_kwargs={}, ) @@ -389,7 +389,7 @@ async def test_concurrent_jobs(): pool_worker_ids=["pool"], allocation_method=RunAllocationMethod.ROUND_ROBIN, func_import_path_or_key="add", - send_channel=False, + func_args=(i, i), func_kwargs={}, ) @@ -417,7 +417,7 @@ async def test_async_function(): pool_id="pool", worker_id=0, func_import_path_or_key="async_add", - send_channel=False, + func_args=(10, 20), func_kwargs={}, ) @@ -469,7 +469,7 @@ async def test_flush_pool_stdout(): pool_id="mp_pool", worker_id=0, func_import_path_or_key="mp_print", - send_channel=False, + func_args=("hello",), func_kwargs={}, ) @@ -517,7 +517,7 @@ async def test_flush_all_pool_stdout(): pool_id="mp_pool", worker_id=0, func_import_path_or_key="mp_print", - send_channel=False, + func_args=("proc0",), func_kwargs={}, ) @@ -525,7 +525,7 @@ async def test_flush_all_pool_stdout(): pool_id="mp_pool", worker_id=1, func_import_path_or_key="mp_print", - send_channel=False, + func_args=("proc1",), func_kwargs={}, ) diff --git a/netrun/src/tests/execution_manager/test_execution_manager_remote.py b/netrun/src/tests/execution_manager/test_execution_manager_remote.py index 99785f0c..267636ad 100644 --- a/netrun/src/tests/execution_manager/test_execution_manager_remote.py +++ b/netrun/src/tests/execution_manager/test_execution_manager_remote.py @@ -133,13 +133,9 @@ async def test_remote_pool_run_function(): await client.send( worker_id=0, key=ExecutionManagerProtocolKeys.RUN.value, - data=("msg_2", "add", "run_1", False, (3, 4), {}) + data=("msg_2", "add", "run_1", (3, 4), {}) ) - # Get RUN_STARTED - msg = await client.recv(timeout=10.0) - assert msg.key == ExecutionManagerProtocolKeys.UP_RUN_STARTED.value - # Get result msg = await client.recv(timeout=10.0) assert msg.key == ExecutionManagerProtocolKeys.UP_RUN_RESPONSE.value @@ -171,22 +167,18 @@ async def test_remote_pool_multiple_workers(): await client.send( worker_id=worker_id, key=ExecutionManagerProtocolKeys.RUN.value, - data=(f"msg_run_{worker_id}", "multiply", f"run_{worker_id}", False, (worker_id + 1, 10), {}) + data=(f"msg_run_{worker_id}", "multiply", f"run_{worker_id}", (worker_id + 1, 10), {}) ) - # Collect results (2 RUN_STARTED + 2 RUN_RESPONSE) - started_count = 0 + # Collect results (2 RUN_RESPONSE) results = {} - for _ in range(4): + for _ in range(2): msg = await client.recv(timeout=10.0) - if msg.key == ExecutionManagerProtocolKeys.UP_RUN_STARTED.value: - started_count += 1 - elif msg.key == ExecutionManagerProtocolKeys.UP_RUN_RESPONSE.value: - msg_id = msg.data[0] - result = msg.data[4] - results[msg_id] = result - - assert started_count == 2 + assert msg.key == ExecutionManagerProtocolKeys.UP_RUN_RESPONSE.value + msg_id = msg.data[0] + result = msg.data[4] + results[msg_id] = result + assert results["msg_run_0"] == 10 # 1 * 10 assert results["msg_run_1"] == 20 # 2 * 10 @@ -210,13 +202,9 @@ async def test_remote_pool_function_with_kwargs(): await client.send( worker_id=0, key=ExecutionManagerProtocolKeys.RUN.value, - data=("msg_2", "kwargs_fn", "run_1", False, (5,), {"b": 20, "c": 200}) + data=("msg_2", "kwargs_fn", "run_1", (5,), {"b": 20, "c": 200}) ) - # Get RUN_STARTED - msg = await client.recv(timeout=10.0) - assert msg.key == ExecutionManagerProtocolKeys.UP_RUN_STARTED.value - # Get result msg = await client.recv(timeout=10.0) assert msg.key == ExecutionManagerProtocolKeys.UP_RUN_RESPONSE.value @@ -246,22 +234,18 @@ async def test_remote_pool_concurrent_runs(): await client.send( worker_id=i, key=ExecutionManagerProtocolKeys.RUN.value, - data=(f"msg_run_{i}", "add", f"run_{i}", False, (i, i), {}) + data=(f"msg_run_{i}", "add", f"run_{i}", (i, i), {}) ) - # Collect all results (4 RUN_STARTED + 4 RUN_RESPONSE) - started_count = 0 + # Collect all results (4 RUN_RESPONSE) results = {} - for _ in range(8): + for _ in range(4): msg = await client.recv(timeout=10.0) - if msg.key == ExecutionManagerProtocolKeys.UP_RUN_STARTED.value: - started_count += 1 - elif msg.key == ExecutionManagerProtocolKeys.UP_RUN_RESPONSE.value: - msg_id = msg.data[0] - result = msg.data[4] - results[msg_id] = result - - assert started_count == 4 + assert msg.key == ExecutionManagerProtocolKeys.UP_RUN_RESPONSE.value + msg_id = msg.data[0] + result = msg.data[4] + results[msg_id] = result + # Verify results for i in range(4): assert results[f"msg_run_{i}"] == i + i @@ -277,7 +261,7 @@ async def test_execution_manager_with_remote_pool(): pool_id="remote", worker_id=0, func_import_path_or_key="add", - send_channel=False, + func_args=(3, 4), func_kwargs={}, ) @@ -300,7 +284,7 @@ async def test_execution_manager_remote_multiple_workers(): pool_id="remote", worker_id=worker_id, func_import_path_or_key="multiply", - send_channel=False, + func_args=(worker_id + 1, 10), func_kwargs={}, ) @@ -328,7 +312,7 @@ async def test_execution_manager_remote_kwargs(): pool_id="remote", worker_id=0, func_import_path_or_key="kwargs_fn", - send_channel=False, + func_args=(5,), func_kwargs={"b": 20, "c": 200}, ) @@ -346,7 +330,7 @@ async def test_execution_manager_remote_timestamps(): pool_id="remote", worker_id=0, func_import_path_or_key="slow", - send_channel=False, + func_args=(0.05,), func_kwargs={}, ) diff --git a/netrun/src/tests/execution_manager/test_execution_manager_thread.py b/netrun/src/tests/execution_manager/test_execution_manager_thread.py index f34e919d..1972d85f 100644 --- a/netrun/src/tests/execution_manager/test_execution_manager_thread.py +++ b/netrun/src/tests/execution_manager/test_execution_manager_thread.py @@ -159,7 +159,7 @@ async def test_send_function_and_run(): pool_id="pool", worker_id=0, func_import_path_or_key="add", - send_channel=False, + func_args=(3, 4), func_kwargs={}, ) @@ -188,7 +188,7 @@ async def test_send_function_to_pool(): pool_id="pool", worker_id=worker_id, func_import_path_or_key="multiply", - send_channel=False, + func_args=(worker_id + 1, 10), func_kwargs={}, ) @@ -212,7 +212,7 @@ async def test_job_result_timestamps(): pool_id="pool", worker_id=0, func_import_path_or_key="slow", - send_channel=False, + func_args=(0.1,), func_kwargs={}, ) @@ -240,7 +240,7 @@ async def test_non_serializable_result_for_main_process(): pool_id="pool", worker_id=0, func_import_path_or_key="nonserialized", - send_channel=False, + func_args=(), func_kwargs={}, ) @@ -264,7 +264,7 @@ async def test_function_with_kwargs(): pool_id="pool", worker_id=0, func_import_path_or_key="kwargs_fn", - send_channel=False, + func_args=(1,), func_kwargs={}, ) @@ -275,7 +275,7 @@ async def test_function_with_kwargs(): pool_id="pool", worker_id=0, func_import_path_or_key="kwargs_fn", - send_channel=False, + func_args=(5,), func_kwargs={"b": 20, "c": 200}, ) @@ -299,7 +299,7 @@ async def test_round_robin_allocation(): pool_worker_ids=["pool"], allocation_method=RunAllocationMethod.ROUND_ROBIN, func_import_path_or_key="add", - send_channel=False, + func_args=(i, 1), func_kwargs={}, ) @@ -326,7 +326,7 @@ async def test_random_allocation(): pool_worker_ids=["pool"], allocation_method=RunAllocationMethod.RANDOM, func_import_path_or_key="add", - send_channel=False, + func_args=(i, 1), func_kwargs={}, ) @@ -353,7 +353,7 @@ async def test_allocation_with_specific_workers(): pool_worker_ids=[("pool", 0), ("pool", 2)], allocation_method=RunAllocationMethod.ROUND_ROBIN, func_import_path_or_key="add", - send_channel=False, + func_args=(i, 1), func_kwargs={}, ) @@ -378,7 +378,7 @@ async def test_empty_workers_raises(): pool_worker_ids=[], allocation_method=RunAllocationMethod.ROUND_ROBIN, func_import_path_or_key="add", - send_channel=False, + func_args=(1, 2), func_kwargs={}, ) @@ -413,7 +413,7 @@ async def test_multiple_pools(): pool_id="fast", worker_id=0, func_import_path_or_key="add", - send_channel=False, + func_args=(5, 3), func_kwargs={}, ) @@ -423,7 +423,7 @@ async def test_multiple_pools(): pool_id="slow", worker_id=0, func_import_path_or_key="multiply", - send_channel=False, + func_args=(4, 7), func_kwargs={}, ) @@ -452,7 +452,7 @@ async def test_concurrent_jobs(): pool_worker_ids=["pool"], allocation_method=RunAllocationMethod.ROUND_ROBIN, func_import_path_or_key="add", - send_channel=False, + func_args=(i, i), func_kwargs={}, ) @@ -480,7 +480,7 @@ async def test_async_function(): pool_id="pool", worker_id=0, func_import_path_or_key="async_add", - send_channel=False, + func_args=(10, 20), func_kwargs={}, ) @@ -502,7 +502,7 @@ async def test_main_pool(): pool_id="main", worker_id=0, func_import_path_or_key="add", - send_channel=False, + func_args=(100, 200), func_kwargs={}, ) @@ -530,7 +530,7 @@ async def test_worker_jobs_cleanup_on_cancellation(): pool_id="pool", worker_id=0, func_import_path_or_key="slow", - send_channel=False, + func_args=(30.0,), # 30s — will be cancelled long before func_kwargs={}, ), @@ -554,7 +554,7 @@ async def test_worker_jobs_cleanup_on_cancellation(): pool_worker_ids=["pool"], allocation_method=RunAllocationMethod.LEAST_BUSY, func_import_path_or_key="add", - send_channel=False, + func_args=(1, 2), func_kwargs={}, ) @@ -582,7 +582,7 @@ async def test_worker_exception_does_not_crash_loop(): pool_id="pool", worker_id=0, func_import_path_or_key="error_fn", - send_channel=False, + func_args=(), func_kwargs={}, ), @@ -594,7 +594,7 @@ async def test_worker_exception_does_not_crash_loop(): pool_id="pool", worker_id=0, func_import_path_or_key="add", - send_channel=False, + func_args=(1, 2), func_kwargs={}, ) @@ -620,7 +620,7 @@ async def test_async_subprocess_in_thread_pool(): pool_id="pool", worker_id=0, func_import_path_or_key="subprocess_fn", - send_channel=False, + func_args=(), func_kwargs={}, ), @@ -646,7 +646,7 @@ async def test_nested_async_in_thread_pool(): pool_id="pool", worker_id=0, func_import_path_or_key="nested", - send_channel=False, + func_args=(), func_kwargs={}, ) @@ -683,7 +683,7 @@ def cpu_bound_sync(n: int) -> int: pool_id="pool", worker_id=i, func_import_path_or_key="cpu", - send_channel=False, + func_args=(100_000,), func_kwargs={}, ) @@ -730,7 +730,7 @@ async def waiter() -> str: pool_id="pool", worker_id=0, func_import_path_or_key="setter", - send_channel=False, + func_args=(0.1,), func_kwargs={}, ) @@ -740,7 +740,7 @@ async def waiter() -> str: pool_id="pool", worker_id=1, func_import_path_or_key="waiter", - send_channel=False, + func_args=(), func_kwargs={}, ) @@ -802,7 +802,7 @@ def crash_worker_func(a: int, b: int) -> int: pool_id="pool", worker_id=0, func_import_path_or_key="crash", - send_channel=False, + func_args=(1, 2), func_kwargs={}, ), @@ -815,7 +815,7 @@ def crash_worker_func(a: int, b: int) -> int: pool_id="pool", worker_id=1, func_import_path_or_key="add", - send_channel=False, + func_args=(10, 20), func_kwargs={}, ), diff --git a/netrun/src/tests/net/test_net.py b/netrun/src/tests/net/test_net.py index 4ffb96e6..5a9a7909 100644 --- a/netrun/src/tests/net/test_net.py +++ b/netrun/src/tests/net/test_net.py @@ -1,6 +1,6 @@ # AUTOGENERATED! DO NOT EDIT! File to edit: pts/tests/06_net/test_net.pct.py -__all__ = ['create_simple_graph_config', 'create_simple_net_config', 'test_async_epoch_callback', 'test_async_error_on_thread_pool', 'test_async_exec_func_on_main_pool', 'test_async_exec_func_on_thread_pool', 'test_async_from_function_factory_on_thread_pool', 'test_async_retry_on_thread_pool', 'test_async_subprocess_on_thread_pool', 'test_check_type_distinguishes_modes', 'test_check_type_string_generic_fails', 'test_check_type_string_simple', 'test_check_type_type_object_generic', 'test_check_type_type_object_generic_mismatch', 'test_check_type_type_object_simple', 'test_close_node_func_called_on_net_stop', 'test_concurrent_async_siblings_on_main_pool', 'test_context_cancel_epoch', 'test_context_consume_packet', 'test_context_consume_packet_not_found', 'test_context_create_packet', 'test_context_create_packet_from_value_func', 'test_context_full_workflow', 'test_context_get_execution_result', 'test_context_load_output_port', 'test_context_print_accumulates', 'test_context_print_basic', 'test_context_print_custom_separators', 'test_context_print_echo_stdout', 'test_context_print_empty', 'test_context_print_flush_ignored', 'test_context_print_multiple_timestamps', 'test_context_print_non_string', 'test_context_send_output_salvo', 'test_create_func_preprocessor_from_config', 'test_create_func_preprocessor_from_config_empty', 'test_create_net_func_preprocessor_basic', 'test_create_net_func_preprocessor_captures_exception', 'test_create_net_func_preprocessor_captures_prints', 'test_create_net_func_preprocessor_with_retry_info', 'test_ctx_state_cleared_on_success', 'test_ctx_state_empty_by_default', 'test_ctx_state_persists_across_retries', 'test_ctx_vars_access', 'test_ctx_vars_empty', 'test_ctx_vars_inherit_missing_global_raises', 'test_ctx_vars_merging', 'test_ctx_vars_merging_with_inherit', 'test_dead_letter_queue_after_max_retries', 'test_dead_letter_queue_has_pool_and_worker', 'test_defer_init_delays_init_node_func', 'test_deferred_actions_preserved_in_result', 'test_deferred_queue_add_consume_packet', 'test_deferred_queue_add_create_packet', 'test_deferred_queue_add_load_output_port', 'test_deferred_queue_add_send_output_salvo', 'test_deferred_queue_discard', 'test_deferred_queue_multiple_creates', 'test_depends_on_blocks_until_dependency_completes', 'test_depends_on_circular_raises', 'test_depends_on_invalid_node_raises', 'test_depends_on_multiple_dependencies', 'test_depends_on_none_is_noop', 'test_enable_disable_unknown_node', 'test_epoch_callback_deregistration', 'test_epoch_cancelled_can_be_raised_and_caught', 'test_epoch_cancelled_exception', 'test_epoch_end_on_cancelled', 'test_epoch_error_basic', 'test_epoch_error_can_be_caught', 'test_epoch_error_chaining', 'test_epoch_error_minimal', 'test_epoch_error_str', 'test_epoch_execution_simple_node', 'test_epoch_execution_with_output', 'test_epoch_record_cancellation_lifecycle', 'test_epoch_record_lifecycle_timestamps', 'test_epoch_record_out_salvos_populated', 'test_exception_queue_contains_epoch_error', 'test_execute_node_calls_init_node_func', 'test_execute_node_inside_net_basic', 'test_execute_node_inside_net_source_node', 'test_execute_node_outside_net', 'test_execute_node_outside_net_async_func', 'test_execute_node_salvo_condition_not_satisfied', 'test_execution_manager_get_pool', 'test_factory_node_exec_func_override', 'test_faulthandler_enabled_after_init', 'test_global_max_epochs_default_unlimited', 'test_global_max_epochs_limits_all_nodes', 'test_init_node_func_called_on_net_start', 'test_init_node_func_called_once_with_defer_init', 'test_is_blocked_with_disabled_node', 'test_max_epochs_allows_up_to_limit', 'test_max_epochs_exceeded_exception', 'test_max_epochs_none_unlimited', 'test_max_epochs_one_raises_on_second', 'test_max_epochs_queued_when_not_propagating', 'test_max_parallel_epochs_limits_concurrent_starts', 'test_max_parallel_epochs_none_allows_all', 'test_max_parallel_epochs_per_node', 'test_multiple_epoch_callbacks', 'test_net_check_rate_limit_enforced', 'test_net_check_rate_limit_no_config', 'test_net_check_rate_limit_none_limit', 'test_net_check_rate_limit_window_expires', 'test_net_clear_dead_letter_queue', 'test_net_close_sync', 'test_net_config_default_pool_allocation_method', 'test_net_config_default_pool_allocation_method_default', 'test_net_config_property', 'test_net_config_type_checking', 'test_net_context_manager', 'test_net_creation', 'test_net_dead_letter_queue_empty', 'test_net_dead_letter_queue_returns_copy', 'test_net_from_file_json', 'test_net_from_file_not_found', 'test_net_from_file_unsupported_format', 'test_net_get_epoch_log_empty', 'test_net_get_node_logs_empty', 'test_net_get_startable_epochs', 'test_net_graph_property', 'test_net_handle_print_buffer', 'test_net_init_sync', 'test_net_install_sigint_handler', 'test_net_invalid_pool_type_raises', 'test_net_is_blocked_empty_network', 'test_net_is_blocked_with_running_epochs', 'test_net_pause_and_resume', 'test_net_protocol_keys_uniqueness', 'test_net_protocol_keys_values', 'test_net_run_step', 'test_net_run_until_blocked', 'test_net_running_epochs_empty', 'test_net_start_and_stop', 'test_net_start_background', 'test_net_start_background_already_running', 'test_net_start_twice_raises', 'test_net_with_multiple_pool_types', 'test_net_with_node_execution_configs', 'test_node_disable_at_runtime', 'test_node_disabled_via_config', 'test_node_enable_at_runtime', 'test_node_execution_config_defaults', 'test_node_execution_config_max_epochs_default', 'test_node_execution_config_max_epochs_set', 'test_node_execution_config_new_fields', 'test_node_execution_config_retry_defaults', 'test_node_execution_config_type_checking', 'test_node_execution_config_with_retries', 'test_node_execution_context_creation', 'test_node_execution_context_with_retry_info', 'test_node_execution_result_with_exception', 'test_node_execution_result_with_func_result', 'test_node_failure_context_creation', 'test_node_failure_context_full', 'test_node_failure_context_has_pool_and_worker', 'test_node_failure_context_print', 'test_node_level_override_print', 'test_node_level_override_propagate', 'test_node_scoped_epoch_callbacks', 'test_on_epoch_end_callback', 'test_on_epoch_start_callback', 'test_on_node_failure_callback', 'test_per_node_max_epochs_overrides_global', 'test_per_node_minus_one_overrides_global_limit', 'test_pool_server_context_as_context_manager_still_works', 'test_pool_server_context_log_no_file', 'test_pool_server_context_start_stop', 'test_pool_server_context_start_stop_with_log_file', 'test_preprocessor_handles_cancel_epoch', 'test_preprocessor_type_checking_inheritance', 'test_print_exceptions_includes_pool_worker', 'test_print_exceptions_true_prints_to_stderr', 'test_propagate_exceptions_false_queues', 'test_propagate_exceptions_true_raises', 'test_propagate_exceptions_wraps_in_epoch_error', 'test_request_pool_shutdown_end_to_end', 'test_request_pool_shutdown_non_remote_raises', 'test_resources_multi_slot', 'test_resources_mutual_exclusion', 'test_resources_none_is_noop', 'test_resources_undefined_raises', 'test_retry_calls_gc_collect', 'test_retry_on_failure', 'test_run_on_init_basic', 'test_run_on_init_disabled', 'test_run_on_init_error_no_valid_condition', 'test_run_on_init_outputs_flow_downstream', 'test_run_on_init_skip_via_constructor', 'test_run_on_init_skip_via_start_param', 'test_run_on_init_start_param_overrides_constructor', 'test_serve_pool_accepts_path', 'test_serve_pool_accepts_pathlib_path', 'test_serve_pool_custom_worker_name', 'test_serve_pool_end_to_end_with_net', 'test_serve_pool_log_file_writes', 'test_serve_pool_no_log_file', 'test_serve_pool_returns_pool_server_context', 'test_serve_pool_starts_and_stops_server', 'test_serve_pool_with_log_file', 'test_server_log_callback', 'test_start_stop_with_async_funcs', 'test_stop_not_called_for_unstarted_deferred_node', 'test_streaming_downstream_starts_before_slow_sibling', 'test_streaming_error_propagates', 'test_streaming_linear_pipeline', 'test_streaming_max_parallel_epochs_across_rounds', 'test_timeout_enforcement_raises_epoch_error', 'test_timeout_goes_to_dead_letter_queue', 'test_timeout_none_no_limit', 'test_type_checking_disabled'] +__all__ = ['create_simple_graph_config', 'create_simple_net_config', 'test_async_epoch_callback', 'test_async_error_on_thread_pool', 'test_async_exec_func_on_main_pool', 'test_async_exec_func_on_thread_pool', 'test_async_from_function_factory_on_thread_pool', 'test_async_retry_on_thread_pool', 'test_async_subprocess_on_thread_pool', 'test_check_type_distinguishes_modes', 'test_check_type_string_generic_fails', 'test_check_type_string_simple', 'test_check_type_type_object_generic', 'test_check_type_type_object_generic_mismatch', 'test_check_type_type_object_simple', 'test_close_node_func_called_on_net_stop', 'test_concurrent_async_siblings_on_main_pool', 'test_context_cancel_epoch', 'test_context_consume_packet', 'test_context_consume_packet_not_found', 'test_context_create_packet', 'test_context_create_packet_from_value_func', 'test_context_full_workflow', 'test_context_get_execution_result', 'test_context_load_output_port', 'test_context_print_accumulates', 'test_context_print_basic', 'test_context_print_custom_separators', 'test_context_print_echo_stdout', 'test_context_print_empty', 'test_context_print_flush_ignored', 'test_context_print_multiple_timestamps', 'test_context_print_non_string', 'test_context_send_output_salvo', 'test_create_func_preprocessor_from_config', 'test_create_func_preprocessor_from_config_empty', 'test_create_net_func_preprocessor_basic', 'test_create_net_func_preprocessor_captures_exception', 'test_create_net_func_preprocessor_captures_prints', 'test_create_net_func_preprocessor_with_retry_info', 'test_ctx_state_cleared_on_success', 'test_ctx_state_empty_by_default', 'test_ctx_state_persists_across_retries', 'test_ctx_vars_access', 'test_ctx_vars_empty', 'test_ctx_vars_inherit_missing_global_raises', 'test_ctx_vars_merging', 'test_ctx_vars_merging_with_inherit', 'test_dead_letter_queue_after_max_retries', 'test_dead_letter_queue_has_pool_and_worker', 'test_defer_init_delays_init_node_func', 'test_deferred_actions_preserved_in_result', 'test_deferred_queue_add_consume_packet', 'test_deferred_queue_add_create_packet', 'test_deferred_queue_add_load_output_port', 'test_deferred_queue_add_send_output_salvo', 'test_deferred_queue_discard', 'test_deferred_queue_multiple_creates', 'test_depends_on_blocks_until_dependency_completes', 'test_depends_on_circular_raises', 'test_depends_on_invalid_node_raises', 'test_depends_on_multiple_dependencies', 'test_depends_on_none_is_noop', 'test_enable_disable_unknown_node', 'test_epoch_callback_deregistration', 'test_epoch_cancelled_can_be_raised_and_caught', 'test_epoch_cancelled_exception', 'test_epoch_end_on_cancelled', 'test_epoch_error_basic', 'test_epoch_error_can_be_caught', 'test_epoch_error_chaining', 'test_epoch_error_minimal', 'test_epoch_error_str', 'test_epoch_execution_simple_node', 'test_epoch_execution_with_output', 'test_epoch_record_cancellation_lifecycle', 'test_epoch_record_lifecycle_timestamps', 'test_epoch_record_out_salvos_populated', 'test_exception_queue_contains_epoch_error', 'test_execute_node_calls_init_node_func', 'test_execute_node_inside_net_basic', 'test_execute_node_inside_net_source_node', 'test_execute_node_outside_net', 'test_execute_node_outside_net_async_func', 'test_execute_node_salvo_condition_not_satisfied', 'test_execution_manager_get_pool', 'test_factory_node_exec_func_override', 'test_faulthandler_enabled_after_init', 'test_global_max_epochs_default_unlimited', 'test_global_max_epochs_limits_all_nodes', 'test_init_node_func_called_on_net_start', 'test_init_node_func_called_once_with_defer_init', 'test_is_blocked_with_disabled_node', 'test_max_epochs_allows_up_to_limit', 'test_max_epochs_exceeded_exception', 'test_max_epochs_none_unlimited', 'test_max_epochs_one_raises_on_second', 'test_max_epochs_queued_when_not_propagating', 'test_max_parallel_epochs_limits_concurrent_starts', 'test_max_parallel_epochs_none_allows_all', 'test_max_parallel_epochs_per_node', 'test_multiple_epoch_callbacks', 'test_net_check_rate_limit_enforced', 'test_net_check_rate_limit_no_config', 'test_net_check_rate_limit_none_limit', 'test_net_check_rate_limit_window_expires', 'test_net_clear_dead_letter_queue', 'test_net_close_sync', 'test_net_config_default_pool_allocation_method', 'test_net_config_default_pool_allocation_method_default', 'test_net_config_property', 'test_net_config_type_checking', 'test_net_context_manager', 'test_net_creation', 'test_net_dead_letter_queue_empty', 'test_net_dead_letter_queue_returns_copy', 'test_net_from_file_json', 'test_net_from_file_not_found', 'test_net_from_file_unsupported_format', 'test_net_get_epoch_log_empty', 'test_net_get_node_logs_empty', 'test_net_get_startable_epochs', 'test_net_graph_property', 'test_net_handle_print_buffer', 'test_net_init_sync', 'test_net_install_sigint_handler', 'test_net_invalid_pool_type_raises', 'test_net_is_blocked_empty_network', 'test_net_is_blocked_with_running_epochs', 'test_net_pause_and_resume', 'test_net_run_step', 'test_net_run_until_blocked', 'test_net_running_epochs_empty', 'test_net_start_and_stop', 'test_net_start_background', 'test_net_start_background_already_running', 'test_net_start_twice_raises', 'test_net_with_multiple_pool_types', 'test_net_with_node_execution_configs', 'test_node_disable_at_runtime', 'test_node_disabled_via_config', 'test_node_enable_at_runtime', 'test_node_execution_config_defaults', 'test_node_execution_config_max_epochs_default', 'test_node_execution_config_max_epochs_set', 'test_node_execution_config_new_fields', 'test_node_execution_config_retry_defaults', 'test_node_execution_config_type_checking', 'test_node_execution_config_with_retries', 'test_node_execution_context_creation', 'test_node_execution_context_with_retry_info', 'test_node_execution_result_with_exception', 'test_node_execution_result_with_func_result', 'test_node_failure_context_creation', 'test_node_failure_context_full', 'test_node_failure_context_has_pool_and_worker', 'test_node_failure_context_print', 'test_node_level_override_print', 'test_node_level_override_propagate', 'test_node_scoped_epoch_callbacks', 'test_on_epoch_end_callback', 'test_on_epoch_start_callback', 'test_on_node_failure_callback', 'test_per_node_max_epochs_overrides_global', 'test_per_node_minus_one_overrides_global_limit', 'test_pool_server_context_as_context_manager_still_works', 'test_pool_server_context_log_no_file', 'test_pool_server_context_start_stop', 'test_pool_server_context_start_stop_with_log_file', 'test_preprocessor_handles_cancel_epoch', 'test_preprocessor_type_checking_inheritance', 'test_print_exceptions_includes_pool_worker', 'test_print_exceptions_true_prints_to_stderr', 'test_propagate_exceptions_false_queues', 'test_propagate_exceptions_true_raises', 'test_propagate_exceptions_wraps_in_epoch_error', 'test_request_pool_shutdown_end_to_end', 'test_request_pool_shutdown_non_remote_raises', 'test_resources_multi_slot', 'test_resources_mutual_exclusion', 'test_resources_none_is_noop', 'test_resources_undefined_raises', 'test_retry_calls_gc_collect', 'test_retry_on_failure', 'test_run_on_init_basic', 'test_run_on_init_disabled', 'test_run_on_init_error_no_valid_condition', 'test_run_on_init_outputs_flow_downstream', 'test_run_on_init_skip_via_constructor', 'test_run_on_init_skip_via_start_param', 'test_run_on_init_start_param_overrides_constructor', 'test_serve_pool_accepts_path', 'test_serve_pool_accepts_pathlib_path', 'test_serve_pool_custom_worker_name', 'test_serve_pool_end_to_end_with_net', 'test_serve_pool_log_file_writes', 'test_serve_pool_no_log_file', 'test_serve_pool_returns_pool_server_context', 'test_serve_pool_starts_and_stops_server', 'test_serve_pool_with_log_file', 'test_server_log_callback', 'test_start_stop_with_async_funcs', 'test_stop_not_called_for_unstarted_deferred_node', 'test_streaming_downstream_starts_before_slow_sibling', 'test_streaming_error_propagates', 'test_streaming_linear_pipeline', 'test_streaming_max_parallel_epochs_across_rounds', 'test_timeout_enforcement_raises_epoch_error', 'test_timeout_goes_to_dead_letter_queue', 'test_timeout_none_no_limit', 'test_type_checking_disabled'] # %% pts/tests/06_net/test_net.pct.py 2 import pytest @@ -12,7 +12,6 @@ from netrun.net._net import ( Net, - NetProtocolKeys, NodeExecutionContext, NodeExecutionResult, NodeFailureContext, @@ -48,26 +47,6 @@ from netrun.packets import LazyPacketValueSpec # %% pts/tests/06_net/test_net.pct.py 4 -def test_net_protocol_keys_values(): - """Test that NetProtocolKeys have expected string values.""" - assert NetProtocolKeys.UP_CREATE_PACKET.value == "net:create-packet" - assert NetProtocolKeys.UP_CREATE_PACKET_RESPONSE.value == "net:create-packet-response" - assert NetProtocolKeys.UP_CONSUME_PACKET.value == "net:consume-packet" - assert NetProtocolKeys.UP_CONSUME_PACKET_RESPONSE.value == "net:consume-packet-response" - assert NetProtocolKeys.UP_LOAD_OUTPUT_PORT.value == "net:load-output-port" - assert NetProtocolKeys.UP_LOAD_OUTPUT_PORT_RESPONSE.value == "net:load-output-port-response" - assert NetProtocolKeys.UP_SEND_OUTPUT_SALVO.value == "net:send-salvo" - assert NetProtocolKeys.UP_SEND_OUTPUT_SALVO_RESPONSE.value == "net:send-salvo-response" - assert NetProtocolKeys.UP_CANCEL_EPOCH.value == "net:cancel-epoch" - assert NetProtocolKeys.UP_PRINT_BUFFER.value == "net:print-buffer" - -# %% pts/tests/06_net/test_net.pct.py 6 -def test_net_protocol_keys_uniqueness(): - """Test that all protocol keys have unique values.""" - values = [key.value for key in NetProtocolKeys] - assert len(values) == len(set(values)), "Protocol keys must have unique values" - -# %% pts/tests/06_net/test_net.pct.py 9 def test_node_execution_context_creation(): """Test NodeExecutionContext can be created with required fields.""" ctx = NodeExecutionContext( @@ -83,7 +62,7 @@ def test_node_execution_context_creation(): assert ctx._created_packets == [] assert ctx._consumed_packets == [] -# %% pts/tests/06_net/test_net.pct.py 11 +# %% pts/tests/06_net/test_net.pct.py 6 def test_node_execution_context_with_retry_info(): """Test NodeExecutionContext with retry information.""" timestamps = [datetime.now()] @@ -100,7 +79,7 @@ def test_node_execution_context_with_retry_info(): assert ctx.retry_timestamps == timestamps assert ctx.retry_exceptions == exceptions -# %% pts/tests/06_net/test_net.pct.py 14 +# %% pts/tests/06_net/test_net.pct.py 9 def test_context_print_basic(): """Test basic print capture without config.""" ctx = NodeExecutionContext( @@ -116,7 +95,7 @@ def test_context_print_basic(): assert isinstance(timestamp, datetime) assert message == "Hello World\n" -# %% pts/tests/06_net/test_net.pct.py 16 +# %% pts/tests/06_net/test_net.pct.py 11 def test_context_print_custom_separators(): """Test print with custom sep and end.""" ctx = NodeExecutionContext( @@ -130,7 +109,7 @@ def test_context_print_custom_separators(): assert isinstance(timestamp, datetime) assert message == "a-b-c!" -# %% pts/tests/06_net/test_net.pct.py 18 +# %% pts/tests/06_net/test_net.pct.py 13 def test_context_print_empty(): """Test print with no arguments.""" ctx = NodeExecutionContext( @@ -144,7 +123,7 @@ def test_context_print_empty(): assert isinstance(timestamp, datetime) assert message == "\n" -# %% pts/tests/06_net/test_net.pct.py 20 +# %% pts/tests/06_net/test_net.pct.py 15 def test_context_print_non_string(): """Test print with non-string arguments.""" ctx = NodeExecutionContext( @@ -158,7 +137,7 @@ def test_context_print_non_string(): assert isinstance(timestamp, datetime) assert message == "1 2.5 True None [1, 2, 3]\n" -# %% pts/tests/06_net/test_net.pct.py 22 +# %% pts/tests/06_net/test_net.pct.py 17 def test_context_print_accumulates(): """Test print accumulates messages in buffer with timestamps.""" config = NodeExecutionConfig( @@ -188,7 +167,7 @@ def test_context_print_accumulates(): for timestamp, _ in ctx._print_buffer: assert isinstance(timestamp, datetime) -# %% pts/tests/06_net/test_net.pct.py 24 +# %% pts/tests/06_net/test_net.pct.py 19 def test_context_print_multiple_timestamps(): """Test that each print call captures its own timestamp.""" ctx = NodeExecutionContext( @@ -209,7 +188,7 @@ def test_context_print_multiple_timestamps(): timestamps = [ts for ts, _ in ctx._print_buffer] assert timestamps[0] <= timestamps[1] <= timestamps[2] -# %% pts/tests/06_net/test_net.pct.py 26 +# %% pts/tests/06_net/test_net.pct.py 21 def test_context_print_flush_ignored(): """Test flush parameter is accepted but has no effect (deferred mode).""" ctx = NodeExecutionContext( @@ -225,7 +204,7 @@ def test_context_print_flush_ignored(): _, message = ctx._print_buffer[0] assert message == "Message with flush\n" -# %% pts/tests/06_net/test_net.pct.py 28 +# %% pts/tests/06_net/test_net.pct.py 23 def test_context_print_echo_stdout(capsys): """Test print with stdout echo enabled.""" config = NodeExecutionConfig( @@ -254,7 +233,7 @@ def test_context_print_echo_stdout(capsys): timestamp, message = ctx._print_buffer[0] assert message == "Echo this message\n" -# %% pts/tests/06_net/test_net.pct.py 30 +# %% pts/tests/06_net/test_net.pct.py 25 def test_context_get_execution_result(): """Test _get_execution_result returns correct data.""" ctx = NodeExecutionContext( @@ -279,7 +258,7 @@ def test_context_get_execution_result(): assert deferred_id in result.created_packets assert "input_packet" in result.consumed_packets -# %% pts/tests/06_net/test_net.pct.py 33 +# %% pts/tests/06_net/test_net.pct.py 28 def test_context_create_packet(): """Test create_packet returns deferred ID and queues action.""" ctx = NodeExecutionContext( @@ -303,7 +282,7 @@ def test_context_create_packet(): # Value should be in packet_values assert ctx._deferred_actions.packet_values[packet_id] == {"data": "test"} -# %% pts/tests/06_net/test_net.pct.py 35 +# %% pts/tests/06_net/test_net.pct.py 30 def test_context_create_packet_from_value_func(): """Test create_packet_from_value_func queues LazyPacketValueSpec.""" ctx = NodeExecutionContext( @@ -332,7 +311,7 @@ def test_context_create_packet_from_value_func(): assert sent_value.args == ("arg1",) assert sent_value.kwargs == {"key": "value"} -# %% pts/tests/06_net/test_net.pct.py 37 +# %% pts/tests/06_net/test_net.pct.py 32 def test_context_consume_packet(): """Test consume_packet returns value from input packets and queues action.""" ctx = NodeExecutionContext( @@ -354,7 +333,7 @@ def test_context_consume_packet(): assert action_type == "consume_packet" assert args == ("packet_xyz",) -# %% pts/tests/06_net/test_net.pct.py 39 +# %% pts/tests/06_net/test_net.pct.py 34 def test_context_consume_packet_not_found(): """Test consume_packet raises KeyError for unknown packet.""" ctx = NodeExecutionContext( @@ -367,7 +346,7 @@ def test_context_consume_packet_not_found(): assert "nonexistent_packet" in str(exc_info.value) -# %% pts/tests/06_net/test_net.pct.py 41 +# %% pts/tests/06_net/test_net.pct.py 36 def test_context_load_output_port(): """Test load_output_port queues action.""" ctx = NodeExecutionContext( @@ -383,7 +362,7 @@ def test_context_load_output_port(): assert action_type == "load_output_port" assert args == ("out", "packet_123") -# %% pts/tests/06_net/test_net.pct.py 43 +# %% pts/tests/06_net/test_net.pct.py 38 def test_context_send_output_salvo(): """Test send_output_salvo queues action.""" ctx = NodeExecutionContext( @@ -399,7 +378,7 @@ def test_context_send_output_salvo(): assert action_type == "send_output_salvo" assert args == ("send_condition",) -# %% pts/tests/06_net/test_net.pct.py 45 +# %% pts/tests/06_net/test_net.pct.py 40 def test_context_cancel_epoch(): """Test cancel_epoch raises EpochCancelled and discards actions.""" ctx = NodeExecutionContext( @@ -420,7 +399,7 @@ def test_context_cancel_epoch(): assert len(ctx._deferred_actions.actions) == 0 assert ctx._cancelled is True -# %% pts/tests/06_net/test_net.pct.py 47 +# %% pts/tests/06_net/test_net.pct.py 42 def test_context_full_workflow(): """Test a complete workflow with create, consume, load, and send.""" ctx = NodeExecutionContext( @@ -456,7 +435,7 @@ def test_context_full_workflow(): assert "input_pkt" in result.consumed_packets assert output_id in result.created_packets -# %% pts/tests/06_net/test_net.pct.py 50 +# %% pts/tests/06_net/test_net.pct.py 45 def test_check_type_string_simple(): """Test string-based type checking with simple type names.""" ctx = NodeExecutionContext(epoch_id="test", node_name="Test") @@ -483,7 +462,7 @@ def test_check_type_string_simple(): assert matches is False assert mode == "string" -# %% pts/tests/06_net/test_net.pct.py 52 +# %% pts/tests/06_net/test_net.pct.py 47 def test_check_type_string_generic_fails(): """Test that string generic types like 'list[int]' fail with __name__ matching. @@ -498,7 +477,7 @@ def test_check_type_string_generic_fails(): assert matches is False # Expected! __name__ is "list", not "list[int]" assert mode == "string" -# %% pts/tests/06_net/test_net.pct.py 54 +# %% pts/tests/06_net/test_net.pct.py 49 def test_check_type_type_object_simple(): """Test type-based checking with simple type objects.""" ctx = NodeExecutionContext(epoch_id="test", node_name="Test") @@ -525,7 +504,7 @@ def test_check_type_type_object_simple(): assert matches is False assert mode == "beartype" -# %% pts/tests/06_net/test_net.pct.py 56 +# %% pts/tests/06_net/test_net.pct.py 51 def test_check_type_type_object_generic(): """Test type-based checking with generic types like list[int]. @@ -554,7 +533,7 @@ def test_check_type_type_object_generic(): assert matches is True assert mode == "beartype" -# %% pts/tests/06_net/test_net.pct.py 58 +# %% pts/tests/06_net/test_net.pct.py 53 def test_check_type_type_object_generic_mismatch(): """Test that generic type mismatches are detected by beartype. @@ -572,7 +551,7 @@ def test_check_type_type_object_generic_mismatch(): assert matches is False assert mode == "beartype" -# %% pts/tests/06_net/test_net.pct.py 60 +# %% pts/tests/06_net/test_net.pct.py 55 def test_check_type_distinguishes_modes(): """Test that string vs type object triggers different checking modes.""" ctx = NodeExecutionContext(epoch_id="test", node_name="Test") @@ -597,7 +576,7 @@ def test_check_type_distinguishes_modes(): assert matches4 is True assert mode4 == "beartype" -# %% pts/tests/06_net/test_net.pct.py 62 +# %% pts/tests/06_net/test_net.pct.py 57 def test_type_checking_disabled(): """Test that type checking can be disabled via _type_checking_enabled.""" from netrun.net._net import PacketTypeMismatch @@ -629,7 +608,7 @@ def test_type_checking_disabled(): ctx_disabled._validate_port_type("out", "int", "not an int", "pkt1") # If we get here without exception, the test passes -# %% pts/tests/06_net/test_net.pct.py 64 +# %% pts/tests/06_net/test_net.pct.py 59 def test_node_execution_config_type_checking(): """Test type_checking_enabled field in NodeExecutionConfig.""" # Default should be None (inherit from Net) @@ -643,7 +622,7 @@ def test_node_execution_config_type_checking(): config_disabled = NodeExecutionConfig(type_checking_enabled=False) assert config_disabled.type_checking_enabled is False -# %% pts/tests/06_net/test_net.pct.py 66 +# %% pts/tests/06_net/test_net.pct.py 61 def test_net_config_type_checking(): """Test type_checking_enabled field in NetConfig.""" # Default should be True @@ -657,7 +636,7 @@ def test_net_config_type_checking(): ) assert config_disabled.type_checking_enabled is False -# %% pts/tests/06_net/test_net.pct.py 68 +# %% pts/tests/06_net/test_net.pct.py 63 def test_preprocessor_type_checking_inheritance(): """Test that preprocessor correctly inherits/overrides type_checking_enabled.""" from types import SimpleNamespace @@ -697,7 +676,7 @@ def test_preprocessor_type_checking_inheritance(): assert preprocessor_disabled._node_configs["enabled_node"].type_checking_enabled is True assert preprocessor_disabled._node_configs["disabled_node"].type_checking_enabled is False -# %% pts/tests/06_net/test_net.pct.py 71 +# %% pts/tests/06_net/test_net.pct.py 66 def test_node_failure_context_creation(): """Test NodeFailureContext creation.""" exc = ValueError("test error") @@ -715,7 +694,7 @@ def test_node_failure_context_creation(): assert ctx.exception == exc assert ctx.input_salvo == {"in": ["packet_1", "packet_2"]} -# %% pts/tests/06_net/test_net.pct.py 74 +# %% pts/tests/06_net/test_net.pct.py 69 def test_deferred_queue_add_create_packet(): """Test adding create_packet action to deferred queue.""" queue = DeferredActionQueue() @@ -728,7 +707,7 @@ def test_deferred_queue_add_create_packet(): assert deferred_id in queue.packet_values assert queue.packet_values[deferred_id] == {"value": 123} -# %% pts/tests/06_net/test_net.pct.py 76 +# %% pts/tests/06_net/test_net.pct.py 71 def test_deferred_queue_add_consume_packet(): """Test adding consume_packet action to deferred queue.""" queue = DeferredActionQueue() @@ -738,7 +717,7 @@ def test_deferred_queue_add_consume_packet(): assert len(queue.actions) == 1 assert queue.actions[0] == ("consume_packet", ("packet_to_consume",)) -# %% pts/tests/06_net/test_net.pct.py 78 +# %% pts/tests/06_net/test_net.pct.py 73 def test_deferred_queue_add_load_output_port(): """Test adding load_output_port action to deferred queue.""" queue = DeferredActionQueue() @@ -748,7 +727,7 @@ def test_deferred_queue_add_load_output_port(): assert len(queue.actions) == 1 assert queue.actions[0] == ("load_output_port", ("out", "packet_123")) -# %% pts/tests/06_net/test_net.pct.py 80 +# %% pts/tests/06_net/test_net.pct.py 75 def test_deferred_queue_add_send_output_salvo(): """Test adding send_output_salvo action to deferred queue.""" queue = DeferredActionQueue() @@ -758,7 +737,7 @@ def test_deferred_queue_add_send_output_salvo(): assert len(queue.actions) == 1 assert queue.actions[0] == ("send_output_salvo", ("send_condition",)) -# %% pts/tests/06_net/test_net.pct.py 82 +# %% pts/tests/06_net/test_net.pct.py 77 def test_deferred_queue_discard(): """Test discarding all queued actions.""" queue = DeferredActionQueue() @@ -776,7 +755,7 @@ def test_deferred_queue_discard(): assert len(queue.packet_values) == 0 assert len(queue.deferred_to_real_ids) == 0 -# %% pts/tests/06_net/test_net.pct.py 84 +# %% pts/tests/06_net/test_net.pct.py 79 def test_deferred_queue_multiple_creates(): """Test creating multiple packets with deferred queue.""" queue = DeferredActionQueue() @@ -791,7 +770,7 @@ def test_deferred_queue_multiple_creates(): # All should start with deferred_ assert all(id_.startswith("deferred_") for id_ in [id1, id2, id3]) -# %% pts/tests/06_net/test_net.pct.py 87 +# %% pts/tests/06_net/test_net.pct.py 82 @pytest.mark.asyncio async def test_create_net_func_preprocessor_basic(): """Test func_preprocessor transforms function correctly.""" @@ -830,7 +809,7 @@ def test_func(ctx, packets): assert call_log[0][1] == "TestNode" assert call_log[0][2] == {"in": ["p1", "p2"]} -# %% pts/tests/06_net/test_net.pct.py 89 +# %% pts/tests/06_net/test_net.pct.py 84 @pytest.mark.asyncio async def test_create_net_func_preprocessor_with_retry_info(): """Test func_preprocessor passes retry information.""" @@ -864,7 +843,7 @@ def test_func(ctx, packets): assert captured_ctx.retry_exceptions == retry_exc assert result.func_result == "ok" -# %% pts/tests/06_net/test_net.pct.py 91 +# %% pts/tests/06_net/test_net.pct.py 86 @pytest.mark.asyncio async def test_create_net_func_preprocessor_captures_prints(): """Test func_preprocessor captures print buffer in result.""" @@ -897,7 +876,7 @@ def test_func(ctx, packets): assert "Message 2\n" in messages assert result.func_result == "done" -# %% pts/tests/06_net/test_net.pct.py 93 +# %% pts/tests/06_net/test_net.pct.py 88 @pytest.mark.asyncio async def test_create_net_func_preprocessor_captures_exception(): """Test func_preprocessor captures exception in result.""" @@ -933,7 +912,7 @@ def test_func(ctx, packets): _, message = result.print_buffer[0] assert message == "Before error\n" -# %% pts/tests/06_net/test_net.pct.py 96 +# %% pts/tests/06_net/test_net.pct.py 91 def create_simple_graph_config(): """Helper to create a simple graph config for testing.""" return GraphConfig( @@ -969,7 +948,7 @@ def create_simple_graph_config(): ], ) -# %% pts/tests/06_net/test_net.pct.py 97 +# %% pts/tests/06_net/test_net.pct.py 92 def create_simple_net_config(): """Helper to create a simple net config for testing.""" return NetConfig( @@ -982,7 +961,7 @@ def create_simple_net_config(): graph=create_simple_graph_config(), ) -# %% pts/tests/06_net/test_net.pct.py 98 +# %% pts/tests/06_net/test_net.pct.py 93 def test_net_creation(): """Test Net can be created with valid config.""" config = create_simple_net_config() @@ -994,7 +973,7 @@ def test_net_creation(): assert net.initialized is False assert net.paused is False -# %% pts/tests/06_net/test_net.pct.py 100 +# %% pts/tests/06_net/test_net.pct.py 95 def test_net_config_property(): """Test Net config property returns correct config.""" config = create_simple_net_config() @@ -1003,7 +982,7 @@ def test_net_config_property(): assert net.config is config assert net.config.pools == config.pools -# %% pts/tests/06_net/test_net.pct.py 102 +# %% pts/tests/06_net/test_net.pct.py 97 def test_net_graph_property(): """Test Net _graph property returns netrun_sim.Graph.""" import netrun_sim @@ -1013,7 +992,7 @@ def test_net_graph_property(): assert isinstance(net._graph, netrun_sim.Graph) -# %% pts/tests/06_net/test_net.pct.py 104 +# %% pts/tests/06_net/test_net.pct.py 99 def test_net_with_multiple_pool_types(): """Test Net can be created with multiple pool types.""" config = NetConfig( @@ -1027,7 +1006,7 @@ def test_net_with_multiple_pool_types(): net = Net(config) assert len(config.pools) == 2 -# %% pts/tests/06_net/test_net.pct.py 106 +# %% pts/tests/06_net/test_net.pct.py 101 def test_net_with_node_execution_configs(): """Test Net extracts node execution configs from graph.""" def dummy_func(ctx, packets): @@ -1071,7 +1050,7 @@ def dummy_func(ctx, packets): assert "NodeA" in net._node_execution_configs assert "NodeB" not in net._node_execution_configs -# %% pts/tests/06_net/test_net.pct.py 108 +# %% pts/tests/06_net/test_net.pct.py 103 @pytest.mark.asyncio async def test_net_start_and_stop(): """Test Net can be started and stopped.""" @@ -1086,7 +1065,7 @@ async def test_net_start_and_stop(): await net.close() assert net.initialized is False -# %% pts/tests/06_net/test_net.pct.py 109 +# %% pts/tests/06_net/test_net.pct.py 104 @pytest.mark.asyncio async def test_net_start_twice_raises(): """Test starting Net twice raises RuntimeError.""" @@ -1102,7 +1081,7 @@ async def test_net_start_twice_raises(): await net.close() -# %% pts/tests/06_net/test_net.pct.py 110 +# %% pts/tests/06_net/test_net.pct.py 105 @pytest.mark.asyncio async def test_net_context_manager(): """Test Net can be used as async context manager.""" @@ -1113,7 +1092,7 @@ async def test_net_context_manager(): assert net.initialized is False -# %% pts/tests/06_net/test_net.pct.py 111 +# %% pts/tests/06_net/test_net.pct.py 106 @pytest.mark.asyncio async def test_net_pause_and_resume(): """Test Net can be paused and resumed.""" @@ -1128,7 +1107,7 @@ async def test_net_pause_and_resume(): net.resume() assert net.paused is False -# %% pts/tests/06_net/test_net.pct.py 112 +# %% pts/tests/06_net/test_net.pct.py 107 @pytest.mark.asyncio async def test_net_run_step(): """Test Net.run_step executes simulation step.""" @@ -1144,7 +1123,7 @@ async def test_net_run_step(): assert isinstance(net_actions, list) assert isinstance(epoch_logs, list) -# %% pts/tests/06_net/test_net.pct.py 113 +# %% pts/tests/06_net/test_net.pct.py 108 @pytest.mark.asyncio async def test_net_run_until_blocked(): """Test Net.run_until_blocked runs until no progress.""" @@ -1157,7 +1136,7 @@ async def test_net_run_until_blocked(): assert isinstance(net_actions, list) assert isinstance(epoch_logs, list) -# %% pts/tests/06_net/test_net.pct.py 114 +# %% pts/tests/06_net/test_net.pct.py 109 @pytest.mark.asyncio async def test_net_get_startable_epochs(): """Test Net.get_startable_epochs returns list.""" @@ -1167,7 +1146,7 @@ async def test_net_get_startable_epochs(): startable = net.get_startable_epochs() assert isinstance(startable, list) -# %% pts/tests/06_net/test_net.pct.py 115 +# %% pts/tests/06_net/test_net.pct.py 110 @pytest.mark.asyncio async def test_net_running_epochs_empty(): """Test Net._running_epochs is empty initially.""" @@ -1177,7 +1156,7 @@ async def test_net_running_epochs_empty(): running = net._running_epochs assert len(running) == 0 # No epochs running initially -# %% pts/tests/06_net/test_net.pct.py 116 +# %% pts/tests/06_net/test_net.pct.py 111 def test_net_get_epoch_log_empty(): """Test logs.for_epoch returns empty list for unknown epoch.""" config = create_simple_net_config() @@ -1186,7 +1165,7 @@ def test_net_get_epoch_log_empty(): log = net.logs.for_epoch("nonexistent_epoch") assert log == [] -# %% pts/tests/06_net/test_net.pct.py 118 +# %% pts/tests/06_net/test_net.pct.py 113 def test_net_get_node_logs_empty(): """Test logs.for_node returns empty list for unknown node.""" config = create_simple_net_config() @@ -1195,7 +1174,7 @@ def test_net_get_node_logs_empty(): log = net.logs.for_node("NonexistentNode") assert log == [] -# %% pts/tests/06_net/test_net.pct.py 120 +# %% pts/tests/06_net/test_net.pct.py 115 def test_net_handle_print_buffer(): """Test Net._handle_print_buffer stores prints correctly.""" from datetime import timezone @@ -1224,7 +1203,7 @@ def test_net_handle_print_buffer(): assert log[0][0] == ts1 assert log[1][0] == ts2 -# %% pts/tests/06_net/test_net.pct.py 123 +# %% pts/tests/06_net/test_net.pct.py 118 def test_net_check_rate_limit_no_config(): """Test rate limiting allows when no config exists.""" config = create_simple_net_config() @@ -1233,7 +1212,7 @@ def test_net_check_rate_limit_no_config(): # No rate limit configured assert net._check_rate_limit("AnyNode") is True -# %% pts/tests/06_net/test_net.pct.py 125 +# %% pts/tests/06_net/test_net.pct.py 120 def test_net_check_rate_limit_none_limit(): """Test rate limiting allows when limit is None.""" def dummy(ctx, packets): @@ -1271,7 +1250,7 @@ def dummy(ctx, packets): for _ in range(10): assert net._check_rate_limit("LimitedNode") is True -# %% pts/tests/06_net/test_net.pct.py 127 +# %% pts/tests/06_net/test_net.pct.py 122 def test_net_check_rate_limit_enforced(): """Test rate limiting enforces limit per second.""" def dummy(ctx, packets): @@ -1313,7 +1292,7 @@ def dummy(ctx, packets): # 4th should be blocked assert net._check_rate_limit("RateLimited") is False -# %% pts/tests/06_net/test_net.pct.py 129 +# %% pts/tests/06_net/test_net.pct.py 124 def test_net_check_rate_limit_window_expires(): """Test rate limit window expires after 1 second.""" def dummy(ctx, packets): @@ -1358,7 +1337,7 @@ def dummy(ctx, packets): # Should be allowed again assert net._check_rate_limit("WindowNode") is True -# %% pts/tests/06_net/test_net.pct.py 132 +# %% pts/tests/06_net/test_net.pct.py 127 def test_node_execution_config_new_fields(): """Test NodeExecutionConfig has config fields.""" config = NodeExecutionConfig( @@ -1370,7 +1349,7 @@ def test_node_execution_config_new_fields(): assert config.print_echo_stdout is True assert config.pool_allocation_method == RunAllocationMethod.LEAST_BUSY -# %% pts/tests/06_net/test_net.pct.py 134 +# %% pts/tests/06_net/test_net.pct.py 129 def test_node_execution_config_defaults(): """Test NodeExecutionConfig has correct defaults.""" config = NodeExecutionConfig() @@ -1378,7 +1357,7 @@ def test_node_execution_config_defaults(): assert config.print_echo_stdout is None assert config.pool_allocation_method is None -# %% pts/tests/06_net/test_net.pct.py 136 +# %% pts/tests/06_net/test_net.pct.py 131 def test_net_config_default_pool_allocation_method(): """Test NetConfig has default_pool_allocation_method field.""" config = NetConfig( @@ -1389,7 +1368,7 @@ def test_net_config_default_pool_allocation_method(): assert config.default_pool_allocation_method == RunAllocationMethod.RANDOM -# %% pts/tests/06_net/test_net.pct.py 138 +# %% pts/tests/06_net/test_net.pct.py 133 def test_net_config_default_pool_allocation_method_default(): """Test NetConfig.default_pool_allocation_method defaults to ROUND_ROBIN.""" config = NetConfig( @@ -1399,7 +1378,7 @@ def test_net_config_default_pool_allocation_method_default(): assert config.default_pool_allocation_method == RunAllocationMethod.ROUND_ROBIN -# %% pts/tests/06_net/test_net.pct.py 141 +# %% pts/tests/06_net/test_net.pct.py 136 def test_epoch_cancelled_exception(): """Test EpochCancelled exception.""" exc = EpochCancelled("Epoch xyz cancelled") @@ -1407,13 +1386,13 @@ def test_epoch_cancelled_exception(): assert str(exc) == "Epoch xyz cancelled" assert isinstance(exc, Exception) -# %% pts/tests/06_net/test_net.pct.py 143 +# %% pts/tests/06_net/test_net.pct.py 138 def test_epoch_cancelled_can_be_raised_and_caught(): """Test EpochCancelled can be raised and caught.""" with pytest.raises(EpochCancelled): raise EpochCancelled("test") -# %% pts/tests/06_net/test_net.pct.py 146 +# %% pts/tests/06_net/test_net.pct.py 141 def test_net_invalid_pool_type_raises(): """Test Net raises ValueError for invalid pool type.""" # Create a mock pool config with invalid type @@ -1427,7 +1406,7 @@ def model_dump(self): # how strictly pydantic validates pass # Skip this test as pydantic prevents invalid pool types -# %% pts/tests/06_net/test_net.pct.py 149 +# %% pts/tests/06_net/test_net.pct.py 144 def test_net_init_sync(): """Test Net.init_sync works (synchronous wrapper).""" config = create_simple_net_config() @@ -1436,7 +1415,7 @@ def test_net_init_sync(): # This would actually start the net, so we just test it's callable assert callable(net.init_sync) -# %% pts/tests/06_net/test_net.pct.py 151 +# %% pts/tests/06_net/test_net.pct.py 146 def test_net_close_sync(): """Test Net.close_sync is callable.""" config = create_simple_net_config() @@ -1444,7 +1423,7 @@ def test_net_close_sync(): assert callable(net.close_sync) -# %% pts/tests/06_net/test_net.pct.py 154 +# %% pts/tests/06_net/test_net.pct.py 149 def test_net_dead_letter_queue_empty(): """Test dead letter queue is initially empty.""" config = create_simple_net_config() @@ -1452,7 +1431,7 @@ def test_net_dead_letter_queue_empty(): assert net.dead_letter_queue == [] -# %% pts/tests/06_net/test_net.pct.py 156 +# %% pts/tests/06_net/test_net.pct.py 151 def test_net_dead_letter_queue_returns_copy(): """Test dead_letter_queue returns a copy, not the internal list.""" config = create_simple_net_config() @@ -1470,7 +1449,7 @@ def test_net_dead_letter_queue_returns_copy(): # Internal queue should not be affected assert len(net._dead_letter_queue) == 1 -# %% pts/tests/06_net/test_net.pct.py 158 +# %% pts/tests/06_net/test_net.pct.py 153 def test_net_clear_dead_letter_queue(): """Test clear_dead_letter_queue returns items and clears queue.""" config = create_simple_net_config() @@ -1490,7 +1469,7 @@ def test_net_clear_dead_letter_queue(): # Queue should now be empty assert net.dead_letter_queue == [] -# %% pts/tests/06_net/test_net.pct.py 161 +# %% pts/tests/06_net/test_net.pct.py 156 def test_node_execution_config_retry_defaults(): """Test NodeExecutionConfig retry defaults (None means inherit from net-level).""" config = NodeExecutionConfig() @@ -1499,7 +1478,7 @@ def test_node_execution_config_retry_defaults(): assert config.retry_wait is None assert config.on_node_failure is None -# %% pts/tests/06_net/test_net.pct.py 163 +# %% pts/tests/06_net/test_net.pct.py 158 def test_node_execution_config_with_retries(): """Test NodeExecutionConfig with retry settings.""" config = NodeExecutionConfig( @@ -1511,7 +1490,7 @@ def test_node_execution_config_with_retries(): assert config.retries == 3 assert config.retry_wait == 0.5 -# %% pts/tests/06_net/test_net.pct.py 165 +# %% pts/tests/06_net/test_net.pct.py 160 def test_node_failure_context_full(): """Test NodeFailureContext with all fields.""" ts1 = datetime.now() @@ -1537,7 +1516,7 @@ def test_node_failure_context_full(): assert ctx.retry_exceptions == [exc1, exc2] assert ctx.input_salvo == {"in": ["p1", "p2"]} -# %% pts/tests/06_net/test_net.pct.py 168 +# %% pts/tests/06_net/test_net.pct.py 163 @pytest.mark.asyncio async def test_net_start_background(): """Test Net can start in background mode.""" @@ -1561,7 +1540,7 @@ async def test_net_start_background(): await net.close() assert not net.initialized -# %% pts/tests/06_net/test_net.pct.py 169 +# %% pts/tests/06_net/test_net.pct.py 164 @pytest.mark.asyncio async def test_net_start_background_already_running(): """Test start_background raises if background task already running.""" @@ -1584,7 +1563,7 @@ async def test_net_start_background_already_running(): finally: await net.close() -# %% pts/tests/06_net/test_net.pct.py 170 +# %% pts/tests/06_net/test_net.pct.py 165 def test_net_is_blocked_empty_network(): """Test is_blocked returns True for empty network.""" graph_config = GraphConfig( @@ -1600,7 +1579,7 @@ def test_net_is_blocked_empty_network(): net = Net(config) assert net.is_blocked() -# %% pts/tests/06_net/test_net.pct.py 171 +# %% pts/tests/06_net/test_net.pct.py 166 def test_net_is_blocked_with_running_epochs(): """Test is_blocked returns False when epochs are running.""" graph_config = GraphConfig( @@ -1617,7 +1596,7 @@ def test_net_is_blocked_with_running_epochs(): net._running_epochs.add("epoch_123") assert not net.is_blocked() -# %% pts/tests/06_net/test_net.pct.py 172 +# %% pts/tests/06_net/test_net.pct.py 167 def test_net_install_sigint_handler(): """Test _install_sigint_handler sets up handler.""" graph_config = GraphConfig( @@ -1636,7 +1615,7 @@ def test_net_install_sigint_handler(): net._restore_sigint_handler() assert net._original_sigint_handler is None -# %% pts/tests/06_net/test_net.pct.py 174 +# %% pts/tests/06_net/test_net.pct.py 169 @pytest.mark.asyncio async def test_epoch_execution_simple_node(): """Test executing a simple node function through the full flow.""" @@ -1701,7 +1680,7 @@ def simple_node(ctx, packets): # Run until blocked to trigger epoch creation and execute epochs await net.run_until_blocked() -# %% pts/tests/06_net/test_net.pct.py 175 +# %% pts/tests/06_net/test_net.pct.py 170 @pytest.mark.asyncio async def test_epoch_execution_with_output(): """Test node that creates output packets.""" @@ -1747,7 +1726,7 @@ def producer_node(ctx, packets): await net.run_until_blocked() # Infrastructure test - verifies Net can be created and run -# %% pts/tests/06_net/test_net.pct.py 176 +# %% pts/tests/06_net/test_net.pct.py 171 @pytest.mark.asyncio async def test_epoch_record_out_salvos_populated(): """Test EpochRecord.out_salvos is populated after epoch execution.""" @@ -1803,7 +1782,7 @@ def producer_node(ctx, packets): assert record.node_name == "Producer" assert len(record.out_salvos) == 1 # One salvo sent -# %% pts/tests/06_net/test_net.pct.py 177 +# %% pts/tests/06_net/test_net.pct.py 172 @pytest.mark.asyncio async def test_epoch_record_lifecycle_timestamps(): """Test EpochRecord lifecycle timestamps are set after execution.""" @@ -1850,7 +1829,7 @@ def simple_node(ctx, packets): import netrun_sim assert record.state == netrun_sim.EpochState.Finished -# %% pts/tests/06_net/test_net.pct.py 178 +# %% pts/tests/06_net/test_net.pct.py 173 @pytest.mark.asyncio async def test_epoch_record_cancellation_lifecycle(): """Test EpochRecord tracks cancellation.""" @@ -1888,7 +1867,7 @@ def cancelling_node(ctx, packets): assert record.ended_at is not None assert record.started_at is not None -# %% pts/tests/06_net/test_net.pct.py 180 +# %% pts/tests/06_net/test_net.pct.py 175 @pytest.mark.asyncio async def test_retry_on_failure(): """Test that node failures trigger retries.""" @@ -1939,7 +1918,7 @@ def failing_node(ctx, packets): assert "FailingNode" in net._node_execution_configs assert net._node_execution_configs["FailingNode"].retries == 3 -# %% pts/tests/06_net/test_net.pct.py 181 +# %% pts/tests/06_net/test_net.pct.py 176 @pytest.mark.asyncio async def test_dead_letter_queue_after_max_retries(): """Test that failed epochs go to dead letter queue after max retries.""" @@ -1984,7 +1963,7 @@ def always_fails(ctx, packets): # Dead letter queue starts empty assert len(net.dead_letter_queue) == 0 -# %% pts/tests/06_net/test_net.pct.py 182 +# %% pts/tests/06_net/test_net.pct.py 177 @pytest.mark.asyncio async def test_on_node_failure_callback(): """Test on_node_failure callback is called on failure.""" @@ -2038,7 +2017,7 @@ def failing_node(ctx, packets): # Verify callback is stored assert net._node_execution_configs["CallbackNode"].on_node_failure is failure_callback -# %% pts/tests/06_net/test_net.pct.py 183 +# %% pts/tests/06_net/test_net.pct.py 178 def test_node_execution_result_with_exception(): """Test NodeExecutionResult correctly stores exception.""" exc = ValueError("test error") @@ -2056,7 +2035,7 @@ def test_node_execution_result_with_exception(): assert result.func_result is None assert not result.cancelled -# %% pts/tests/06_net/test_net.pct.py 185 +# %% pts/tests/06_net/test_net.pct.py 180 def test_node_execution_result_with_func_result(): """Test NodeExecutionResult correctly stores function result.""" result = NodeExecutionResult( @@ -2074,7 +2053,7 @@ def test_node_execution_result_with_func_result(): assert result.created_packets == ["pkt1"] assert result.consumed_packets == ["pkt2"] -# %% pts/tests/06_net/test_net.pct.py 187 +# %% pts/tests/06_net/test_net.pct.py 182 @pytest.mark.asyncio async def test_preprocessor_handles_cancel_epoch(): """Test preprocessor handles EpochCancelled correctly.""" @@ -2102,7 +2081,7 @@ def cancelling_func(ctx, packets): assert len(result.print_buffer) == 1 assert "Before cancel" in result.print_buffer[0][1] -# %% pts/tests/06_net/test_net.pct.py 189 +# %% pts/tests/06_net/test_net.pct.py 184 @pytest.mark.asyncio async def test_deferred_actions_preserved_in_result(): """Test that deferred actions are preserved in execution result.""" @@ -2133,7 +2112,7 @@ def action_func(ctx, packets): assert len(result.created_packets) == 2 assert len(result.deferred_actions.actions) == 4 # 2 creates + load + send -# %% pts/tests/06_net/test_net.pct.py 191 +# %% pts/tests/06_net/test_net.pct.py 186 def test_ctx_vars_access(): """Test ctx.vars returns resolved variable values.""" ctx = NodeExecutionContext( @@ -2155,7 +2134,7 @@ def test_ctx_vars_access(): assert v["debug"] is True assert v["config"] == {"key": "val"} -# %% pts/tests/06_net/test_net.pct.py 193 +# %% pts/tests/06_net/test_net.pct.py 188 def test_ctx_vars_empty(): """Test ctx.vars returns empty dict when no vars set.""" ctx = NodeExecutionContext( @@ -2166,7 +2145,7 @@ def test_ctx_vars_empty(): assert ctx.vars == {} assert isinstance(ctx.vars, dict) -# %% pts/tests/06_net/test_net.pct.py 195 +# %% pts/tests/06_net/test_net.pct.py 190 @pytest.mark.asyncio async def test_ctx_vars_merging(): """Test that node-level vars override net-level vars via preprocessor.""" @@ -2214,7 +2193,7 @@ def test_func(ctx, packets): # Net-only var assert v["net_only"] is True -# %% pts/tests/06_net/test_net.pct.py 197 +# %% pts/tests/06_net/test_net.pct.py 192 @pytest.mark.asyncio async def test_ctx_vars_merging_with_inherit(): """Test inherit=True vars use global type/options in preprocessor merge.""" @@ -2266,7 +2245,7 @@ def test_func(ctx, packets): # Global-only var assert v["global_only"] is True -# %% pts/tests/06_net/test_net.pct.py 199 +# %% pts/tests/06_net/test_net.pct.py 194 def test_ctx_vars_inherit_missing_global_raises(): """inherit=True without matching global var raises ValueError.""" node_configs = { @@ -2283,7 +2262,7 @@ def test_ctx_vars_inherit_missing_global_raises(): net_node_vars=None, ) -# %% pts/tests/06_net/test_net.pct.py 202 +# %% pts/tests/06_net/test_net.pct.py 197 def test_net_from_file_json(tmp_path): """Test Net.from_file loads a Net from a JSON config file.""" import json @@ -2307,13 +2286,13 @@ def test_net_from_file_json(tmp_path): assert net.config is not None assert net.initialized is False -# %% pts/tests/06_net/test_net.pct.py 203 +# %% pts/tests/06_net/test_net.pct.py 198 def test_net_from_file_not_found(): """Test Net.from_file raises FileNotFoundError for missing file.""" with pytest.raises(FileNotFoundError): Net.from_file("/nonexistent/path/config.json") -# %% pts/tests/06_net/test_net.pct.py 205 +# %% pts/tests/06_net/test_net.pct.py 200 def test_net_from_file_unsupported_format(tmp_path): """Test Net.from_file raises ValueError for unsupported format.""" bad_file = tmp_path / "config.yaml" @@ -2322,7 +2301,7 @@ def test_net_from_file_unsupported_format(tmp_path): with pytest.raises(ValueError, match="Unsupported"): Net.from_file(bad_file) -# %% pts/tests/06_net/test_net.pct.py 207 +# %% pts/tests/06_net/test_net.pct.py 202 @pytest.mark.asyncio async def test_propagate_exceptions_true_raises(): """Test that run_until_blocked raises EpochError when propagate_exceptions=True (default).""" @@ -2360,7 +2339,7 @@ def failing_node(ctx, packets): assert isinstance(exc_info.value.__cause__, ValueError) assert "node failure" in str(exc_info.value.__cause__) -# %% pts/tests/06_net/test_net.pct.py 208 +# %% pts/tests/06_net/test_net.pct.py 203 @pytest.mark.asyncio async def test_propagate_exceptions_false_queues(): """Test that run_until_blocked succeeds when propagate_exceptions=False, and EpochError is queued.""" @@ -2410,7 +2389,7 @@ def failing_node(ctx, packets): # Queue should now be empty assert len(net.exception_queue) == 0 -# %% pts/tests/06_net/test_net.pct.py 209 +# %% pts/tests/06_net/test_net.pct.py 204 @pytest.mark.asyncio async def test_print_exceptions_true_prints_to_stderr(capsys): """Test that print_exceptions=True prints to stderr with pool/worker info.""" @@ -2450,7 +2429,7 @@ def failing_node(ctx, packets): assert "FailNode" in captured.err assert "pool='main'" in captured.err -# %% pts/tests/06_net/test_net.pct.py 210 +# %% pts/tests/06_net/test_net.pct.py 205 @pytest.mark.asyncio async def test_node_level_override_propagate(): """Test per-node override of propagate_exceptions.""" @@ -2507,7 +2486,7 @@ def failing_node(ctx, packets): assert isinstance(exc_info.value.__cause__, ValueError) assert "PropagatingNode failed" in str(exc_info.value.__cause__) -# %% pts/tests/06_net/test_net.pct.py 211 +# %% pts/tests/06_net/test_net.pct.py 206 @pytest.mark.asyncio async def test_node_level_override_print(capsys): """Test per-node override of print_exceptions.""" @@ -2565,7 +2544,7 @@ def failing_node(ctx, packets): assert "PrintingNode" in captured.err assert "PrintingNode failed" in captured.err -# %% pts/tests/06_net/test_net.pct.py 213 +# %% pts/tests/06_net/test_net.pct.py 208 from netrun.net._net._net import _PoolServerContext, _ServerLogCallback from netrun.net.config import RemotePoolConfig from ..net.workers import doubler_node, echo_node @@ -2578,7 +2557,7 @@ def _get_next_serve_pool_port() -> int: _serve_pool_test_port += 1 return _serve_pool_test_port -# %% pts/tests/06_net/test_net.pct.py 214 +# %% pts/tests/06_net/test_net.pct.py 209 def test_serve_pool_returns_pool_server_context(): """Test that serve_pool returns a _PoolServerContext instance.""" config = NetConfig( @@ -2591,7 +2570,7 @@ def test_serve_pool_returns_pool_server_context(): assert ctx._port == 9999 assert ctx._server is not None -# %% pts/tests/06_net/test_net.pct.py 216 +# %% pts/tests/06_net/test_net.pct.py 211 def test_serve_pool_accepts_path(tmp_path): """Test that serve_pool accepts a file path string.""" import json @@ -2606,7 +2585,7 @@ def test_serve_pool_accepts_path(tmp_path): ctx = Net.serve_pool(str(config_file), "127.0.0.1", 9999) assert isinstance(ctx, _PoolServerContext) -# %% pts/tests/06_net/test_net.pct.py 217 +# %% pts/tests/06_net/test_net.pct.py 212 def test_serve_pool_accepts_pathlib_path(tmp_path): """Test that serve_pool accepts a pathlib.Path.""" import json @@ -2622,7 +2601,7 @@ def test_serve_pool_accepts_pathlib_path(tmp_path): ctx = Net.serve_pool(config_file, "127.0.0.1", 9999) assert isinstance(ctx, _PoolServerContext) -# %% pts/tests/06_net/test_net.pct.py 218 +# %% pts/tests/06_net/test_net.pct.py 213 def test_serve_pool_custom_worker_name(): """Test that serve_pool passes custom worker_name to the server.""" config = NetConfig( @@ -2632,7 +2611,7 @@ def test_serve_pool_custom_worker_name(): ctx = Net.serve_pool(config, "127.0.0.1", 9999, worker_name="custom_worker") assert isinstance(ctx, _PoolServerContext) -# %% pts/tests/06_net/test_net.pct.py 220 +# %% pts/tests/06_net/test_net.pct.py 215 def test_serve_pool_no_log_file(): """Test that serve_pool without log_file does not create a done callback.""" config = NetConfig( @@ -2643,7 +2622,7 @@ def test_serve_pool_no_log_file(): assert ctx._log_file is None assert ctx._log_fh is None -# %% pts/tests/06_net/test_net.pct.py 222 +# %% pts/tests/06_net/test_net.pct.py 217 def test_serve_pool_with_log_file(tmp_path): """Test that serve_pool with log_file configures logging.""" log_file = tmp_path / "server.log" @@ -2656,7 +2635,7 @@ def test_serve_pool_with_log_file(tmp_path): # File handle not opened until __aenter__ assert ctx._log_fh is None -# %% pts/tests/06_net/test_net.pct.py 223 +# %% pts/tests/06_net/test_net.pct.py 218 @pytest.mark.asyncio async def test_serve_pool_starts_and_stops_server(): """Test that serve_pool context manager starts and stops a working server.""" @@ -2681,7 +2660,7 @@ async def test_serve_pool_starts_and_stops_server(): # After exiting context, server should be stopped -# %% pts/tests/06_net/test_net.pct.py 224 +# %% pts/tests/06_net/test_net.pct.py 219 @pytest.mark.asyncio async def test_serve_pool_log_file_writes(tmp_path): """Test that serve_pool writes start/stop messages to log file.""" @@ -2702,7 +2681,7 @@ async def test_serve_pool_log_file_writes(tmp_path): content = log_file.read_text() assert "Pool server stopped" in content -# %% pts/tests/06_net/test_net.pct.py 225 +# %% pts/tests/06_net/test_net.pct.py 220 @pytest.mark.asyncio async def test_serve_pool_end_to_end_with_net(): """Test serve_pool with a Net that has a remote pool — full integration. @@ -2774,7 +2753,7 @@ async def test_serve_pool_end_to_end_with_net(): values.extend(queue_vals) assert 10 in values -# %% pts/tests/06_net/test_net.pct.py 226 +# %% pts/tests/06_net/test_net.pct.py 221 def test_create_func_preprocessor_from_config(): """Test _create_func_preprocessor_from_config returns a callable preprocessor.""" def dummy_exec(ctx): @@ -2811,7 +2790,7 @@ def dummy_exec(ctx): preprocessor = Net._create_func_preprocessor_from_config(config_resolved) assert callable(preprocessor) -# %% pts/tests/06_net/test_net.pct.py 228 +# %% pts/tests/06_net/test_net.pct.py 223 def test_create_func_preprocessor_from_config_empty(): """Test _create_func_preprocessor_from_config with empty graph.""" config = NetConfig( @@ -2822,14 +2801,14 @@ def test_create_func_preprocessor_from_config_empty(): preprocessor = Net._create_func_preprocessor_from_config(config_resolved) assert callable(preprocessor) -# %% pts/tests/06_net/test_net.pct.py 230 +# %% pts/tests/06_net/test_net.pct.py 225 def test_pool_server_context_log_no_file(): """Test _PoolServerContext._log is a no-op when no log file is open.""" ctx = _PoolServerContext(None, "127.0.0.1", 8080, log_file=None) # Should not raise ctx._log("test message") -# %% pts/tests/06_net/test_net.pct.py 232 +# %% pts/tests/06_net/test_net.pct.py 227 def test_server_log_callback(): """Test _ServerLogCallback is callable and picklable.""" import pickle @@ -2842,7 +2821,7 @@ def test_server_log_callback(): restored = pickle.loads(pickled) assert callable(restored) -# %% pts/tests/06_net/test_net.pct.py 235 +# %% pts/tests/06_net/test_net.pct.py 230 def test_epoch_error_basic(): """Test EpochError fields and isinstance.""" err = EpochError("bad input", node_name="MyNode", epoch_id="ep-123", @@ -2855,7 +2834,7 @@ def test_epoch_error_basic(): assert err.worker_id == 2 assert err.retry_count == 1 -# %% pts/tests/06_net/test_net.pct.py 237 +# %% pts/tests/06_net/test_net.pct.py 232 def test_epoch_error_str(): """Test EpochError __str__ includes node, pool, worker, and cause.""" cause = ValueError("bad input") @@ -2869,7 +2848,7 @@ def test_epoch_error_str(): assert "retries=1" in s assert "bad input" in s -# %% pts/tests/06_net/test_net.pct.py 239 +# %% pts/tests/06_net/test_net.pct.py 234 def test_epoch_error_minimal(): """Test EpochError with only required fields and defaults.""" err = EpochError("fail", node_name="N", epoch_id="e1") @@ -2883,13 +2862,13 @@ def test_epoch_error_minimal(): assert "worker=" not in s assert "retries=" not in s -# %% pts/tests/06_net/test_net.pct.py 241 +# %% pts/tests/06_net/test_net.pct.py 236 def test_epoch_error_can_be_caught(): """Test that EpochError can be raised and caught.""" with pytest.raises(EpochError): raise EpochError("fail", node_name="N", epoch_id="e1") -# %% pts/tests/06_net/test_net.pct.py 243 +# %% pts/tests/06_net/test_net.pct.py 238 def test_epoch_error_chaining(): """Test raise ... from preserves __cause__.""" original = ValueError("original") @@ -2899,7 +2878,7 @@ def test_epoch_error_chaining(): assert e.__cause__ is original assert isinstance(e.__cause__, ValueError) -# %% pts/tests/06_net/test_net.pct.py 245 +# %% pts/tests/06_net/test_net.pct.py 240 @pytest.mark.asyncio async def test_propagate_exceptions_wraps_in_epoch_error(): """Integration: run_until_blocked raises EpochError with correct fields.""" @@ -2941,7 +2920,7 @@ def failing_node(ctx, packets): assert isinstance(err.__cause__, RuntimeError) assert "kaboom" in str(err.__cause__) -# %% pts/tests/06_net/test_net.pct.py 246 +# %% pts/tests/06_net/test_net.pct.py 241 @pytest.mark.asyncio async def test_exception_queue_contains_epoch_error(): """Integration: queued exceptions are EpochError with __cause__.""" @@ -2981,7 +2960,7 @@ def failing_node(ctx, packets): assert isinstance(err.__cause__, TypeError) assert "bad type" in str(err.__cause__) -# %% pts/tests/06_net/test_net.pct.py 247 +# %% pts/tests/06_net/test_net.pct.py 242 @pytest.mark.asyncio async def test_dead_letter_queue_has_pool_and_worker(): """Test dead letter queue entries include pool_id and worker_id.""" @@ -3019,7 +2998,7 @@ def always_fails(ctx, packets): assert entry["pool_id"] == "main" assert entry["node_name"] == "DLQ" -# %% pts/tests/06_net/test_net.pct.py 248 +# %% pts/tests/06_net/test_net.pct.py 243 @pytest.mark.asyncio async def test_node_failure_context_has_pool_and_worker(): """Test on_node_failure callback receives pool_id and worker_id.""" @@ -3065,7 +3044,7 @@ def failing_node(ctx, packets): assert failure_log[0]["worker_id"] is not None assert failure_log[0]["node_name"] == "CBNode" -# %% pts/tests/06_net/test_net.pct.py 249 +# %% pts/tests/06_net/test_net.pct.py 244 @pytest.mark.asyncio async def test_print_exceptions_includes_pool_worker(capsys): """Test that stderr output includes pool and worker info.""" @@ -3104,7 +3083,7 @@ def failing_node(ctx, packets): assert "pool='main'" in captured.err assert "print test" in captured.err -# %% pts/tests/06_net/test_net.pct.py 251 +# %% pts/tests/06_net/test_net.pct.py 246 @pytest.mark.asyncio async def test_pool_server_context_start_stop(): """Test _PoolServerContext.start() and stop() work without context manager.""" @@ -3131,7 +3110,7 @@ async def test_pool_server_context_start_stop(): await pool_ctx.stop() -# %% pts/tests/06_net/test_net.pct.py 252 +# %% pts/tests/06_net/test_net.pct.py 247 @pytest.mark.asyncio async def test_pool_server_context_as_context_manager_still_works(): """Test _PoolServerContext still works as async context manager.""" @@ -3153,7 +3132,7 @@ async def test_pool_server_context_as_context_manager_still_works(): await client.create_pool("execution_manager", num_processes=1, threads_per_process=1) await client.close() -# %% pts/tests/06_net/test_net.pct.py 253 +# %% pts/tests/06_net/test_net.pct.py 248 @pytest.mark.asyncio async def test_request_pool_shutdown_end_to_end(): """Test that Net.request_pool_shutdown signals the server to stop.""" @@ -3193,7 +3172,7 @@ async def test_request_pool_shutdown_end_to_end(): await pool_ctx.stop() -# %% pts/tests/06_net/test_net.pct.py 254 +# %% pts/tests/06_net/test_net.pct.py 249 @pytest.mark.asyncio async def test_request_pool_shutdown_non_remote_raises(): """Test request_pool_shutdown raises TypeError for non-remote pools.""" @@ -3206,7 +3185,7 @@ async def test_request_pool_shutdown_non_remote_raises(): with pytest.raises(TypeError, match="not a RemotePoolClient"): await net.request_pool_shutdown("main") -# %% pts/tests/06_net/test_net.pct.py 255 +# %% pts/tests/06_net/test_net.pct.py 250 @pytest.mark.asyncio async def test_execution_manager_get_pool(): """Test ExecutionManager.get_pool returns the pool instance.""" @@ -3224,7 +3203,7 @@ async def test_execution_manager_get_pool(): with pytest.raises(KeyError, match="not found"): manager.get_pool("nonexistent") -# %% pts/tests/06_net/test_net.pct.py 256 +# %% pts/tests/06_net/test_net.pct.py 251 @pytest.mark.asyncio async def test_pool_server_context_start_stop_with_log_file(tmp_path): """Test start/stop writes log messages to file.""" @@ -3246,7 +3225,7 @@ async def test_pool_server_context_start_stop_with_log_file(tmp_path): content = log_file.read_text() assert "Pool server stopped" in content -# %% pts/tests/06_net/test_net.pct.py 258 +# %% pts/tests/06_net/test_net.pct.py 253 def test_max_epochs_exceeded_exception(): """Test MaxEpochsExceeded exception has correct attributes.""" exc = MaxEpochsExceeded("MyNode", 3) @@ -3255,19 +3234,19 @@ def test_max_epochs_exceeded_exception(): assert "MyNode" in str(exc) assert "3" in str(exc) -# %% pts/tests/06_net/test_net.pct.py 260 +# %% pts/tests/06_net/test_net.pct.py 255 def test_node_execution_config_max_epochs_default(): """Test max_epochs defaults to None (unlimited).""" config = NodeExecutionConfig() assert config.max_epochs is None -# %% pts/tests/06_net/test_net.pct.py 262 +# %% pts/tests/06_net/test_net.pct.py 257 def test_node_execution_config_max_epochs_set(): """Test max_epochs can be set.""" config = NodeExecutionConfig(max_epochs=1) assert config.max_epochs == 1 -# %% pts/tests/06_net/test_net.pct.py 264 +# %% pts/tests/06_net/test_net.pct.py 259 @pytest.mark.asyncio async def test_max_epochs_allows_up_to_limit(): """Test that a node with max_epochs=2 can run exactly 2 epochs.""" @@ -3311,7 +3290,7 @@ def counting_node(ctx, packets): assert results == [10, 20] -# %% pts/tests/06_net/test_net.pct.py 265 +# %% pts/tests/06_net/test_net.pct.py 260 @pytest.mark.asyncio async def test_max_epochs_one_raises_on_second(): """Test that max_epochs=1 raises EpochError on second epoch.""" @@ -3355,7 +3334,7 @@ def simple_node(ctx, packets): assert exc_info.value.__cause__.node_name == "OnceOnly" assert exc_info.value.__cause__.max_epochs == 1 -# %% pts/tests/06_net/test_net.pct.py 266 +# %% pts/tests/06_net/test_net.pct.py 261 @pytest.mark.asyncio async def test_max_epochs_none_unlimited(): """Test that max_epochs=None allows unlimited epochs.""" @@ -3396,7 +3375,7 @@ def counting_node(ctx, packets): assert count == 10 -# %% pts/tests/06_net/test_net.pct.py 267 +# %% pts/tests/06_net/test_net.pct.py 262 @pytest.mark.asyncio async def test_max_epochs_queued_when_not_propagating(): """Test that MaxEpochsExceeded is queued when propagate_exceptions=False.""" @@ -3441,13 +3420,13 @@ def simple_node(ctx, packets): assert isinstance(net._exception_queue[0], EpochError) assert isinstance(net._exception_queue[0].__cause__, MaxEpochsExceeded) -# %% pts/tests/06_net/test_net.pct.py 269 +# %% pts/tests/06_net/test_net.pct.py 264 def test_global_max_epochs_default_unlimited(): """Test that NetConfig() has max_epochs=-1 (unlimited) by default.""" config = NetConfig(graph=GraphConfig(nodes=[], edges=[])) assert config.max_epochs == -1 -# %% pts/tests/06_net/test_net.pct.py 271 +# %% pts/tests/06_net/test_net.pct.py 266 @pytest.mark.asyncio async def test_global_max_epochs_limits_all_nodes(): """Test that global max_epochs limits nodes without per-node config.""" @@ -3492,7 +3471,7 @@ def simple_node(ctx, packets): assert isinstance(exc_info.value.__cause__, MaxEpochsExceeded) assert exc_info.value.__cause__.max_epochs == 2 -# %% pts/tests/06_net/test_net.pct.py 272 +# %% pts/tests/06_net/test_net.pct.py 267 @pytest.mark.asyncio async def test_per_node_max_epochs_overrides_global(): """Test that per-node max_epochs overrides global max_epochs.""" @@ -3538,7 +3517,7 @@ def simple_node(ctx, packets): assert isinstance(exc_info.value.__cause__, MaxEpochsExceeded) assert exc_info.value.__cause__.max_epochs == 2 -# %% pts/tests/06_net/test_net.pct.py 273 +# %% pts/tests/06_net/test_net.pct.py 268 @pytest.mark.asyncio async def test_per_node_minus_one_overrides_global_limit(): """Test that per-node max_epochs=-1 makes node unlimited despite global limit.""" @@ -3581,7 +3560,7 @@ def counting_node(ctx, packets): assert count == 5 -# %% pts/tests/06_net/test_net.pct.py 275 +# %% pts/tests/06_net/test_net.pct.py 270 @pytest.mark.asyncio async def test_factory_node_exec_func_override(): """Test that exec_node_func in execution_config overrides the factory's function.""" @@ -3626,7 +3605,7 @@ def override_exec(ctx, packets): record = list(net._epochs.values())[0] assert len(record.out_salvos) == 0 -# %% pts/tests/06_net/test_net.pct.py 277 +# %% pts/tests/06_net/test_net.pct.py 272 @pytest.mark.asyncio async def test_timeout_enforcement_raises_epoch_error(): """Test that timeout on NodeExecutionConfig is enforced via asyncio.wait_for. @@ -3688,7 +3667,7 @@ def slow_node(ctx, packets): # Release the worker thread so pool cleanup completes quickly stop_event.set() -# %% pts/tests/06_net/test_net.pct.py 278 +# %% pts/tests/06_net/test_net.pct.py 273 @pytest.mark.asyncio async def test_timeout_goes_to_dead_letter_queue(): """Test that a timed-out epoch goes to the dead letter queue.""" @@ -3750,7 +3729,7 @@ def slow_node(ctx, packets): finally: stop_event.set() -# %% pts/tests/06_net/test_net.pct.py 279 +# %% pts/tests/06_net/test_net.pct.py 274 @pytest.mark.asyncio async def test_timeout_none_no_limit(): """Test that timeout=None (default) means no timeout is applied.""" @@ -3797,7 +3776,7 @@ def fast_node(ctx, packets): assert len(net.exception_queue) == 0 assert len(net.dead_letter_queue) == 0 -# %% pts/tests/06_net/test_net.pct.py 281 +# %% pts/tests/06_net/test_net.pct.py 276 @pytest.mark.asyncio async def test_max_parallel_epochs_limits_concurrent_starts(): """Test that max_parallel_epochs prevents starting too many epochs in a single run_step. @@ -3867,7 +3846,7 @@ def tracked_node(ctx, packets): # All 3 packets should have been processed eventually assert sorted(execution_order) == [10, 20, 30] -# %% pts/tests/06_net/test_net.pct.py 282 +# %% pts/tests/06_net/test_net.pct.py 277 @pytest.mark.asyncio async def test_max_parallel_epochs_none_allows_all(): """Test that max_parallel_epochs=None (default) allows all startable epochs.""" @@ -3927,7 +3906,7 @@ def tracked_node(ctx, packets): assert sorted(execution_order) == [10, 20, 30] -# %% pts/tests/06_net/test_net.pct.py 283 +# %% pts/tests/06_net/test_net.pct.py 278 @pytest.mark.asyncio async def test_max_parallel_epochs_per_node(): """Test that max_parallel_epochs is enforced per-node, not globally.""" @@ -4022,7 +4001,7 @@ def node_b_func(ctx, packets): assert sorted(results_a) == [1, 2] assert sorted(results_b) == [3, 4] -# %% pts/tests/06_net/test_net.pct.py 285 +# %% pts/tests/06_net/test_net.pct.py 280 @pytest.mark.asyncio async def test_init_node_func_called_on_net_start(): """Test that init_node_func is called when the Net starts.""" @@ -4061,7 +4040,7 @@ def my_exec(ctx, packets): # init_node_func should have been called during Net.init() assert lifecycle == ["started"] -# %% pts/tests/06_net/test_net.pct.py 286 +# %% pts/tests/06_net/test_net.pct.py 281 @pytest.mark.asyncio async def test_close_node_func_called_on_net_stop(): """Test that close_node_func is called when the Net stops.""" @@ -4106,7 +4085,7 @@ def my_exec(ctx, packets): # close_node_func should have been called during Net.close() assert lifecycle == ["started", "stopped"] -# %% pts/tests/06_net/test_net.pct.py 287 +# %% pts/tests/06_net/test_net.pct.py 282 @pytest.mark.asyncio async def test_defer_init_delays_init_node_func(): """Test that defer_init=True delays init_node_func until first epoch.""" @@ -4161,7 +4140,7 @@ def my_exec(ctx, packets): # stop should be called on Net.close() assert lifecycle == ["started", "executed", "stopped"] -# %% pts/tests/06_net/test_net.pct.py 288 +# %% pts/tests/06_net/test_net.pct.py 283 @pytest.mark.asyncio async def test_start_stop_with_async_funcs(): """Test that async start/stop functions work correctly.""" @@ -4205,7 +4184,7 @@ def my_exec(ctx, packets): assert lifecycle == ["async_started", "async_stopped"] -# %% pts/tests/06_net/test_net.pct.py 289 +# %% pts/tests/06_net/test_net.pct.py 284 @pytest.mark.asyncio async def test_init_node_func_called_once_with_defer_init(): """Test that init_node_func is called exactly once even with multiple epochs.""" @@ -4251,7 +4230,7 @@ def my_exec(ctx, packets): # start should have been called exactly once assert start_count == 1 -# %% pts/tests/06_net/test_net.pct.py 290 +# %% pts/tests/06_net/test_net.pct.py 285 @pytest.mark.asyncio async def test_stop_not_called_for_unstarted_deferred_node(): """Test that close_node_func is NOT called for a deferred node that never ran.""" @@ -4298,7 +4277,7 @@ def my_exec(ctx, packets): # Neither start nor stop should have been called assert lifecycle == [] -# %% pts/tests/06_net/test_net.pct.py 292 +# %% pts/tests/06_net/test_net.pct.py 287 def test_node_failure_context_print(): """Test that NodeFailureContext.print() captures timestamped messages.""" ctx = NodeFailureContext( @@ -4331,7 +4310,7 @@ def test_node_failure_context_print(): timestamps = [ts for ts, _ in ctx._print_buffer] assert timestamps == sorted(timestamps) -# %% pts/tests/06_net/test_net.pct.py 294 +# %% pts/tests/06_net/test_net.pct.py 289 @pytest.mark.asyncio async def test_execute_node_inside_net_basic(): """Test execute_node inside net with a simple node that has inputs.""" @@ -4391,7 +4370,7 @@ def simple_node(ctx, packets): assert result.exception is None assert len(net._epochs) == 1 -# %% pts/tests/06_net/test_net.pct.py 295 +# %% pts/tests/06_net/test_net.pct.py 290 @pytest.mark.asyncio async def test_execute_node_inside_net_source_node(): """Test execute_node for a source node with no inputs (True salvo condition).""" @@ -4444,7 +4423,7 @@ def source_node(ctx, packets): assert result.exception is None assert len(net._epochs) == 1 -# %% pts/tests/06_net/test_net.pct.py 296 +# %% pts/tests/06_net/test_net.pct.py 291 @pytest.mark.asyncio async def test_execute_node_salvo_condition_not_satisfied(): """Test execute_node raises ValueError when no salvo condition is satisfied.""" @@ -4482,7 +4461,7 @@ async def test_execute_node_salvo_condition_not_satisfied(): with pytest.raises(ValueError, match="No input salvo condition satisfied"): await net.execute_node("NeedsInput", inputs={}) -# %% pts/tests/06_net/test_net.pct.py 297 +# %% pts/tests/06_net/test_net.pct.py 292 @pytest.mark.asyncio async def test_execute_node_outside_net(): """Test execute_node with outside_net=True returns dict of output values.""" @@ -4543,7 +4522,7 @@ def transform_node(ctx, packets): # No epochs should be created in netsim assert len(net._epochs) == 0 -# %% pts/tests/06_net/test_net.pct.py 298 +# %% pts/tests/06_net/test_net.pct.py 293 @pytest.mark.asyncio async def test_execute_node_outside_net_async_func(): """Test execute_node outside net with an async exec function.""" @@ -4601,7 +4580,7 @@ async def async_node(ctx, packets): assert isinstance(result, dict) assert result["out"] == [21] -# %% pts/tests/06_net/test_net.pct.py 299 +# %% pts/tests/06_net/test_net.pct.py 294 @pytest.mark.asyncio async def test_async_exec_func_on_thread_pool(): """Test that an async exec_node_func works on a thread pool worker.""" @@ -4661,7 +4640,7 @@ async def async_doubler(ctx, packets): values.extend(queue_vals) assert 10 in values -# %% pts/tests/06_net/test_net.pct.py 300 +# %% pts/tests/06_net/test_net.pct.py 295 @pytest.mark.asyncio async def test_async_exec_func_on_main_pool(): """Test that an async exec_node_func works on the main pool.""" @@ -4721,7 +4700,7 @@ async def async_tripler(ctx, packets): values.extend(queue_vals) assert 12 in values -# %% pts/tests/06_net/test_net.pct.py 301 +# %% pts/tests/06_net/test_net.pct.py 296 @pytest.mark.asyncio async def test_async_subprocess_on_thread_pool(): """Test that asyncio.create_subprocess_exec works on thread pool (issue #32).""" @@ -4788,7 +4767,7 @@ async def subprocess_node(ctx, packets): values.extend(queue_vals) assert "hello from subprocess" in values -# %% pts/tests/06_net/test_net.pct.py 302 +# %% pts/tests/06_net/test_net.pct.py 297 @pytest.mark.asyncio async def test_async_from_function_factory_on_thread_pool(): """Test async function via from_function factory on thread pool.""" @@ -4818,7 +4797,7 @@ async def async_double(x: int) -> int: values.extend(queue_vals) assert 10 in values -# %% pts/tests/06_net/test_net.pct.py 303 +# %% pts/tests/06_net/test_net.pct.py 298 @pytest.mark.asyncio async def test_async_error_on_thread_pool(): """Test async function error propagation on thread pool.""" @@ -4866,7 +4845,7 @@ async def async_failing(ctx, packets): assert isinstance(exc_info.value.__cause__, ValueError) assert "async error" in str(exc_info.value.__cause__) -# %% pts/tests/06_net/test_net.pct.py 304 +# %% pts/tests/06_net/test_net.pct.py 299 @pytest.mark.asyncio async def test_async_retry_on_thread_pool(): """Test async function retries on thread pool.""" @@ -4935,7 +4914,7 @@ async def async_retry_func(ctx, packets): assert any("success" in str(v) for v in values) assert call_count == 3 # 2 failures + 1 success -# %% pts/tests/06_net/test_net.pct.py 305 +# %% pts/tests/06_net/test_net.pct.py 300 @pytest.mark.asyncio async def test_execute_node_calls_init_node_func(): """Test that execute_node calls init_node_func if not yet called.""" @@ -4976,7 +4955,7 @@ def my_exec(ctx, packets): await net.execute_node("Worker", inputs={"in": [42]}) assert "started" in lifecycle -# %% pts/tests/06_net/test_net.pct.py 307 +# %% pts/tests/06_net/test_net.pct.py 302 @pytest.mark.asyncio async def test_run_on_init_basic(): """Test that a node with run_on_init=True is executed during Net.init().""" @@ -5032,7 +5011,7 @@ def source_node(ctx, packets): assert execution_log == ["executed"] assert len(net._epochs) == 1 -# %% pts/tests/06_net/test_net.pct.py 308 +# %% pts/tests/06_net/test_net.pct.py 303 @pytest.mark.asyncio async def test_run_on_init_error_no_valid_condition(): """Test that run_on_init raises ValueError if no salvo condition is satisfied with empty inputs.""" @@ -5071,7 +5050,7 @@ async def test_run_on_init_error_no_valid_condition(): async with Net(config) as net: pass -# %% pts/tests/06_net/test_net.pct.py 309 +# %% pts/tests/06_net/test_net.pct.py 304 @pytest.mark.asyncio async def test_run_on_init_outputs_flow_downstream(): """Test that run_on_init node outputs flow to downstream nodes.""" @@ -5153,7 +5132,7 @@ def sink_node(ctx, packets): assert "sink" in execution_log assert "consumed:42" in execution_log -# %% pts/tests/06_net/test_net.pct.py 310 +# %% pts/tests/06_net/test_net.pct.py 305 @pytest.mark.asyncio async def test_run_on_init_skip_via_start_param(): """Test that init(run_init_nodes=False) skips init nodes.""" @@ -5214,7 +5193,7 @@ def source_node(ctx, packets): finally: await net.close() -# %% pts/tests/06_net/test_net.pct.py 311 +# %% pts/tests/06_net/test_net.pct.py 306 @pytest.mark.asyncio async def test_run_on_init_skip_via_constructor(): """Test that Net(config, run_init_nodes=False) skips init nodes via context manager.""" @@ -5268,7 +5247,7 @@ def source_node(ctx, packets): # Startup node should NOT have been executed assert execution_log == [] -# %% pts/tests/06_net/test_net.pct.py 312 +# %% pts/tests/06_net/test_net.pct.py 307 @pytest.mark.asyncio async def test_run_on_init_start_param_overrides_constructor(): """Test that start(run_init_nodes=True) overrides constructor's False.""" @@ -5326,7 +5305,7 @@ def source_node(ctx, packets): finally: await net.close() -# %% pts/tests/06_net/test_net.pct.py 314 +# %% pts/tests/06_net/test_net.pct.py 309 @pytest.mark.asyncio async def test_node_disabled_via_config(): """Node with enabled=False in config should not execute epochs.""" @@ -5364,7 +5343,7 @@ def node_func(ctx, packets): assert len(startable) == 1 assert len(execution_log) == 0 -# %% pts/tests/06_net/test_net.pct.py 316 +# %% pts/tests/06_net/test_net.pct.py 311 @pytest.mark.asyncio async def test_node_disable_at_runtime(): """Disabling a node at runtime prevents its epochs from executing.""" @@ -5404,7 +5383,7 @@ def node_func(ctx, packets): assert len(execution_log) == 0 assert len(net.get_startable_epochs()) == 1 -# %% pts/tests/06_net/test_net.pct.py 318 +# %% pts/tests/06_net/test_net.pct.py 313 @pytest.mark.asyncio async def test_node_enable_at_runtime(): """Re-enabling a node allows pending startable epochs to execute.""" @@ -5446,7 +5425,7 @@ def node_func(ctx, packets): assert len(execution_log) == 1 assert execution_log[0] == "A" -# %% pts/tests/06_net/test_net.pct.py 320 +# %% pts/tests/06_net/test_net.pct.py 315 @pytest.mark.asyncio async def test_is_blocked_with_disabled_node(): """is_blocked() returns True when only disabled nodes have startable epochs.""" @@ -5480,7 +5459,7 @@ def node_func(ctx, packets): assert len(net.get_startable_epochs()) == 1 assert net.is_blocked() is True -# %% pts/tests/06_net/test_net.pct.py 322 +# %% pts/tests/06_net/test_net.pct.py 317 @pytest.mark.asyncio async def test_enable_disable_unknown_node(): """enable_node/disable_node/is_node_enabled raise KeyError for unknown nodes.""" @@ -5508,7 +5487,7 @@ async def test_enable_disable_unknown_node(): with pytest.raises(KeyError, match="not found"): net.is_node_enabled("NONEXISTENT") -# %% pts/tests/06_net/test_net.pct.py 324 +# %% pts/tests/06_net/test_net.pct.py 319 @pytest.mark.asyncio async def test_run_on_init_disabled(): """Node with run_on_init=True and enabled=False should NOT run on init.""" @@ -5549,7 +5528,7 @@ def source_func(ctx, packets): # Source should NOT have been executed despite run_on_init assert "source" not in execution_log -# %% pts/tests/06_net/test_net.pct.py 327 +# %% pts/tests/06_net/test_net.pct.py 322 @pytest.mark.asyncio async def test_on_epoch_start_callback(): """Net-level on_epoch_start callback fires with correct args.""" @@ -5590,7 +5569,7 @@ def simple_node(ctx, packets): assert calls[0]["node_name"] == "Node" assert calls[0]["epoch_id"] is not None -# %% pts/tests/06_net/test_net.pct.py 329 +# %% pts/tests/06_net/test_net.pct.py 324 @pytest.mark.asyncio async def test_on_epoch_end_callback(): """Net-level on_epoch_end callback fires with EpochRecord.""" @@ -5639,7 +5618,7 @@ def simple_node(ctx, packets): assert record.started_at is not None assert record.outcome == "success" -# %% pts/tests/06_net/test_net.pct.py 331 +# %% pts/tests/06_net/test_net.pct.py 326 @pytest.mark.asyncio async def test_node_scoped_epoch_callbacks(): """Node-scoped callback only fires for that specific node.""" @@ -5710,7 +5689,7 @@ def node_b_func(ctx, packets): # Only B should have triggered the callback assert calls == ["B"] -# %% pts/tests/06_net/test_net.pct.py 333 +# %% pts/tests/06_net/test_net.pct.py 328 @pytest.mark.asyncio async def test_epoch_callback_deregistration(): """remove() stops future callbacks from firing.""" @@ -5755,7 +5734,7 @@ def simple_node(ctx, packets): # Should still be 1 — callback was removed assert len(calls) == 1 -# %% pts/tests/06_net/test_net.pct.py 335 +# %% pts/tests/06_net/test_net.pct.py 330 @pytest.mark.asyncio async def test_async_epoch_callback(): """Async callbacks are awaited correctly.""" @@ -5800,7 +5779,7 @@ def simple_node(ctx, packets): assert calls[0]["node_name"] == "Node" assert calls[1]["ended"] is True -# %% pts/tests/06_net/test_net.pct.py 337 +# %% pts/tests/06_net/test_net.pct.py 332 @pytest.mark.asyncio async def test_epoch_end_on_cancelled(): """End callback fires with was_cancelled=True when epoch is cancelled.""" @@ -5842,7 +5821,7 @@ def cancelling_node(ctx, packets): assert end_calls[0]["node_name"] == "Canceller" assert end_calls[0]["outcome"] == "cancelled" -# %% pts/tests/06_net/test_net.pct.py 339 +# %% pts/tests/06_net/test_net.pct.py 334 @pytest.mark.asyncio async def test_multiple_epoch_callbacks(): """Multiple callbacks all fire in registration order.""" @@ -5889,7 +5868,7 @@ def simple_node(ctx, packets): assert order == ["cb1", "cb2", "cb3"] -# %% pts/tests/06_net/test_net.pct.py 342 +# %% pts/tests/06_net/test_net.pct.py 337 @pytest.mark.asyncio async def test_streaming_downstream_starts_before_slow_sibling(): """Verify streaming scheduling: downstream of a fast node starts before a slow sibling completes. @@ -6022,7 +6001,7 @@ def downstream_func(ctx, packets): "Streaming scheduling should have allowed downstream to start before slow sibling completed." ) -# %% pts/tests/06_net/test_net.pct.py 344 +# %% pts/tests/06_net/test_net.pct.py 339 @pytest.mark.asyncio async def test_streaming_error_propagates(): """Verify that when one epoch raises with propagate_exceptions=True, the error propagates.""" @@ -6068,7 +6047,7 @@ async def error_func(ctx, packets): with pytest.raises(EpochError, match="test error"): await net.run_until_blocked() -# %% pts/tests/06_net/test_net.pct.py 346 +# %% pts/tests/06_net/test_net.pct.py 341 @pytest.mark.asyncio async def test_streaming_max_parallel_epochs_across_rounds(): """Verify max_parallel_epochs is respected across streaming rounds.""" @@ -6126,7 +6105,7 @@ async def tracked_func(ctx, packets): # All 5 epochs should have executed assert len(concurrency_log) == 5 -# %% pts/tests/06_net/test_net.pct.py 348 +# %% pts/tests/06_net/test_net.pct.py 343 @pytest.mark.asyncio async def test_streaming_linear_pipeline(): """Verify A -> B -> C pipeline: B starts as soon as A finishes, C as soon as B finishes.""" @@ -6243,7 +6222,7 @@ async def node_c(ctx, packets): assert timestamps["b_start"] >= timestamps["a_end"] assert timestamps["c_start"] >= timestamps["b_end"] -# %% pts/tests/06_net/test_net.pct.py 351 +# %% pts/tests/06_net/test_net.pct.py 346 @pytest.mark.asyncio async def test_concurrent_async_siblings_on_main_pool(): """Verify that sibling async nodes on MainPoolConfig execute concurrently. @@ -6365,7 +6344,7 @@ async def node_c(ctx, packets): "Nodes should have overlapping execution windows (concurrent), but they don't" ) -# %% pts/tests/06_net/test_net.pct.py 354 +# %% pts/tests/06_net/test_net.pct.py 349 @pytest.mark.asyncio async def test_faulthandler_enabled_after_init(): """Test that faulthandler is enabled after Net.init().""" @@ -6407,7 +6386,7 @@ def noop_node(ctx, packets): async with Net(config) as net: assert faulthandler.is_enabled() -# %% pts/tests/06_net/test_net.pct.py 357 +# %% pts/tests/06_net/test_net.pct.py 352 @pytest.mark.asyncio async def test_retry_calls_gc_collect(): """Test that gc.collect() is called between retry attempts.""" @@ -6467,7 +6446,7 @@ def failing_then_succeeds(ctx, packets): # The node should have succeeded on 3rd attempt assert call_count[0] == 3 -# %% pts/tests/06_net/test_net.pct.py 360 +# %% pts/tests/06_net/test_net.pct.py 355 @pytest.mark.asyncio async def test_ctx_state_persists_across_retries(): """Test that ctx.state is preserved across retry attempts.""" @@ -6529,7 +6508,7 @@ def stateful_node(ctx, packets): assert call_log[1]["counter"] == 2 # Second attempt (same dict) assert call_log[2]["counter"] == 3 # Third attempt (same dict) -# %% pts/tests/06_net/test_net.pct.py 362 +# %% pts/tests/06_net/test_net.pct.py 357 @pytest.mark.asyncio async def test_ctx_state_empty_by_default(): """Test that ctx.state is an empty dict when not used.""" @@ -6576,7 +6555,7 @@ def check_state(ctx, packets): assert observed_state[0] == {} -# %% pts/tests/06_net/test_net.pct.py 365 +# %% pts/tests/06_net/test_net.pct.py 360 @pytest.mark.asyncio async def test_depends_on_blocks_until_dependency_completes(): """Test that depends_on prevents a node from starting until its dependency completes.""" @@ -6652,7 +6631,7 @@ def node_b(ctx, packets): # A must execute before B (B depends_on A) assert execution_order == ["A", "B"] -# %% pts/tests/06_net/test_net.pct.py 368 +# %% pts/tests/06_net/test_net.pct.py 363 @pytest.mark.asyncio async def test_resources_mutual_exclusion(): """Test that two nodes with the same 1-slot resource never run concurrently.""" @@ -6744,7 +6723,7 @@ async def heavy_b(ctx, packets): f"B=[{timestamps['b_start']:.3f}, {timestamps['b_end']:.3f}]" ) -# %% pts/tests/06_net/test_net.pct.py 370 +# %% pts/tests/06_net/test_net.pct.py 365 @pytest.mark.asyncio async def test_resources_none_is_noop(): """Test that nodes without resources work as before.""" @@ -6791,7 +6770,7 @@ def simple_node(ctx, packets): assert executed[0] is True -# %% pts/tests/06_net/test_net.pct.py 373 +# %% pts/tests/06_net/test_net.pct.py 368 @pytest.mark.asyncio async def test_ctx_state_cleared_on_success(): """Test that ctx.state is cleaned up from internal tracking after epoch succeeds.""" @@ -6841,7 +6820,7 @@ def stateful_node(ctx, packets): f"retry_state should be cleared on success, got {record.retry_state}" ) -# %% pts/tests/06_net/test_net.pct.py 375 +# %% pts/tests/06_net/test_net.pct.py 370 @pytest.mark.asyncio async def test_depends_on_multiple_dependencies(): """Test that a node waits for ALL dependencies to complete.""" @@ -6912,7 +6891,7 @@ def make_node(name, func, depends_on=None): assert "A" in execution_order[:c_idx], "A should complete before C" assert "B" in execution_order[:c_idx], "B should complete before C" -# %% pts/tests/06_net/test_net.pct.py 377 +# %% pts/tests/06_net/test_net.pct.py 372 @pytest.mark.asyncio async def test_depends_on_none_is_noop(): """Test that nodes without depends_on work as before.""" @@ -6959,7 +6938,7 @@ def simple_node(ctx, packets): assert executed[0] is True -# %% pts/tests/06_net/test_net.pct.py 379 +# %% pts/tests/06_net/test_net.pct.py 374 @pytest.mark.asyncio async def test_resources_multi_slot(): """Test that a multi-slot resource allows bounded concurrency.""" @@ -7027,7 +7006,7 @@ def make_gpu_node(name): # but less than 0.25s (which would indicate only 1 slot) assert total >= 0.15, f"Expected >= 0.15s for 2-slot bounded concurrency, got {total:.3f}s" -# %% pts/tests/06_net/test_net.pct.py 381 +# %% pts/tests/06_net/test_net.pct.py 376 @pytest.mark.asyncio async def test_resources_undefined_raises(): """Test that referencing an undefined resource raises an error.""" @@ -7073,7 +7052,7 @@ def simple_node(ctx, packets): with pytest.raises(ValueError, match="nonexistent_resource"): await net.run_until_blocked() -# %% pts/tests/06_net/test_net.pct.py 384 +# %% pts/tests/06_net/test_net.pct.py 379 def test_depends_on_circular_raises(): """Test that circular depends_on raises ValueError at construction.""" def noop(ctx, packets): @@ -7116,7 +7095,7 @@ def make_node(name, depends_on=None): with pytest.raises(ValueError, match="[Cc]ircular"): Net(config) -# %% pts/tests/06_net/test_net.pct.py 386 +# %% pts/tests/06_net/test_net.pct.py 381 def test_depends_on_invalid_node_raises(): """Test that depends_on referencing non-existent node raises ValueError.""" def noop(ctx, packets): From b3d42bbe028294925dc4e6c37831a24ff4a58259 Mon Sep 17 00:00:00 2001 From: Lukas Kikuchi Date: Fri, 1 May 2026 12:41:00 +0100 Subject: [PATCH 12/15] refactor(netrun): extract CLI to netrun-cli package, add --edges/mermaid/dry-run MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prompt to recreate this work: Extract the CLI from `netrun` into a new sibling Python package called `netrun-cli` (following the `netrun-utils` pattern), and add three new CLI commands. Background: the CLI has a clean boundary — its only imports from netrun are `netrun.net.config` and `netrun.tools`, and nothing in `netrun` imports from `netrun.cli`. So extraction is safe. Steps: 1. Create `netrun-cli/` at the repo root with: - `pyproject.toml` declaring `netrun-cli` v0.5.0 depending on `netrun>=0.5.0`, `typer>=0.15.0`, `tomli-w>=1.0`. Register `[project.scripts] netrun = "netrun_cli._app:app_main"`. Use the same ruff/pytest config and uv source pattern as `netrun-utils`. - `nblite.toml` with the standard `nbs -> pts -> lib` pipeline, exporting to `src/netrun_cli` and `src/tests`. - Empty `src/netrun_cli/__init__.py` (hatchling needs it to find the package; nblite does not auto-generate it). 2. Move the seven CLI source files from `netrun/pts/netrun/10_cli/*.pct.py` to `netrun-cli/pts/netrun_cli/`. In each file: - Change `#|default_exp cli._foo` to `#|default_exp _foo`. - Change `from netrun.cli._foo import ...` to `from netrun_cli._foo import ...`. - Leave `netrun.net.config.*`, `netrun.tools.*` imports untouched — those are external dependencies on the `netrun` package. - Leave hardcoded strings (`name="netrun"` in the Typer app, the `.netrun.json/.netrun.toml` extensions, GitHub repo refs in `download_agents`, `netrun.node_factories.broadcast` warning) untouched. 3. Move the two CLI test files from `netrun/pts/tests/10_cli/` to `netrun-cli/pts/tests/`. In each: - Update `#|default_exp` (drop the `cli.` prefix). - Update `from netrun.cli._app import app` -> `from netrun_cli._app import app`. - In `test_cli.pct.py`, the SAMPLE_DIR path traversal goes from `.parent` 5 times to 4 times (one fewer directory level). - Replace all `netrun.cli._download_agents` mock-patch strings with `netrun_cli._download_agents` (~5 occurrences). 4. Run `nbl export --reverse && nbl export` in `netrun-cli/` to populate `nbs/` and `src/`. 5. Run `uv sync` in `netrun-cli/`. Then `uv pip install -e .` to install the `netrun` script entry point. 6. Delete `netrun/pts/netrun/10_cli/`, `netrun/pts/tests/10_cli/`, `netrun/src/netrun/cli/`, `netrun/src/tests/cli/`, and any matching directories under `netrun/nbs/`. 7. Edit `netrun/pyproject.toml`: - Remove `[project.scripts] netrun = "netrun.cli._app:app_main"`. - Remove `cli = ["typer>=0.15.0", "tomli-w>=1.0"]` from `[project.optional-dependencies]`. - Replace `"netrun[cli]"` in the dev dependency group with `"netrun-cli"`. - Add `netrun-cli = { path = "../netrun-cli", editable = true }` under `[tool.uv.sources]`. Then run `uv sync` in `netrun/`. 8. Add three new CLI features in `netrun-cli/pts/netrun_cli/02_commands.pct.py`: - `node --edges`: when the flag is set, include an `edges` key in the JSON output with `incoming` (list of `{source: "node.port", port: ...}`) and `outgoing` (list of `{port: ..., target: "node.port"}`). Iterate `net_config.graph.edges` and filter by `target_node == name` / `source_node == name`. - `structure --format=mermaid|json` (default `json` for back-compat): when `format=mermaid`, emit `graph LR`, then one node line per node `[""]`, then one edge line per edge ` -->|""| `. Sanitise IDs by replacing spaces and hyphens with underscores. Reject any other format with `typer.Exit(1)`. - `dry-run`: new command. Topologically sort the nodes (Kahn's algorithm) using data edges (skip `dependency=True`) plus `depends_on` entries as ordering edges. Emit JSON: `source_nodes` (no incoming), `sink_nodes` (no outgoing), `execution_order` (each entry has `node`, optionally `pools`, `depends_on`, `resources` from execution_config), and `total_nodes` / `total_edges`. Register `dry-run` in `00_app.pct.py` alongside the other top-level commands. 9. Add tests in `netrun-cli/pts/tests/test_cli.pct.py` covering all three: - `test_node_edges_incoming` / `test_node_edges_outgoing` / `test_node_edges_none` / `test_node_no_edges_by_default` - `test_structure_mermaid` (verify `graph LR`, node decls, edge labels) / `test_structure_json_default` / `test_structure_bad_format` - `test_dry_run_basic` / `test_dry_run_execution_order` / `test_dry_run_sink_nodes` / `test_dry_run_depends_on_and_resources` (the last builds a tmp config with depends_on + resources to verify ordering and metadata appear in the output) - Add `assert "dry-run" in result.stdout` to `test_help`. 10. Re-export and run: `cd netrun-cli && nbl export --reverse && nbl export && uv run pytest src/tests/ -v`. All 61 tests should pass. 11. Verify `cd netrun && uv run pytest src/tests/ -q`. All 1305 tests should pass (down from 1355 — the 50 CLI tests moved out). 12. Smoke-test the new features against `sample_projects/00_basic_net_project/main.netrun.json`: - `.venv/bin/netrun node add --edges -c ` shows edges - `.venv/bin/netrun structure --format mermaid -c ` emits Mermaid - `.venv/bin/netrun dry-run -c ` shows topo order Co-Authored-By: Claude Opus 4.7 (1M context) --- netrun-cli/nblite.toml | 31 + netrun-cli/nbs/netrun_cli/00_app.ipynb | 147 ++ netrun-cli/nbs/netrun_cli/01_helpers.ipynb | 293 ++++ netrun-cli/nbs/netrun_cli/02_commands.ipynb | 654 ++++++++ netrun-cli/nbs/netrun_cli/03_actions.ipynb | 156 ++ netrun-cli/nbs/netrun_cli/04_recipes.ipynb | 142 ++ .../nbs/netrun_cli/05_download_agents.ipynb | 124 ++ netrun-cli/nbs/netrun_cli/06_graph.ipynb | 496 ++++++ netrun-cli/nbs/tests/test_cli.ipynb | 794 ++++++++++ netrun-cli/nbs/tests/test_cli_graph.ipynb | 406 +++++ .../pts/netrun_cli}/00_app.pct.py | 14 +- .../pts/netrun_cli}/01_helpers.pct.py | 2 +- .../pts/netrun_cli}/02_commands.pct.py | 137 +- .../pts/netrun_cli}/03_actions.pct.py | 4 +- .../pts/netrun_cli}/04_recipes.pct.py | 4 +- .../pts/netrun_cli}/05_download_agents.pct.py | 2 +- .../pts/netrun_cli}/06_graph.pct.py | 4 +- .../pts/tests}/__init__.py | 0 .../pts/tests}/test_cli.pct.py | 170 ++- .../pts/tests}/test_cli_graph.pct.py | 4 +- netrun-cli/pyproject.toml | 82 + netrun-cli/src/netrun_cli/__init__.py | 0 .../src/netrun_cli}/_actions.py | 14 +- .../cli => netrun-cli/src/netrun_cli}/_app.py | 28 +- .../src/netrun_cli}/_commands.py | 155 +- .../src/netrun_cli}/_download_agents.py | 6 +- .../src/netrun_cli}/_graph.py | 16 +- .../src/netrun_cli}/_helpers.py | 14 +- .../src/netrun_cli}/_recipes.py | 12 +- .../cli => netrun-cli/src/tests}/test_cli.py | 188 ++- .../src/tests}/test_cli_graph.py | 20 +- netrun-cli/uv.lock | 1323 +++++++++++++++++ netrun/pyproject.toml | 7 +- netrun/uv.lock | 1171 ++++++++------- 34 files changed, 5955 insertions(+), 665 deletions(-) create mode 100644 netrun-cli/nblite.toml create mode 100644 netrun-cli/nbs/netrun_cli/00_app.ipynb create mode 100644 netrun-cli/nbs/netrun_cli/01_helpers.ipynb create mode 100644 netrun-cli/nbs/netrun_cli/02_commands.ipynb create mode 100644 netrun-cli/nbs/netrun_cli/03_actions.ipynb create mode 100644 netrun-cli/nbs/netrun_cli/04_recipes.ipynb create mode 100644 netrun-cli/nbs/netrun_cli/05_download_agents.ipynb create mode 100644 netrun-cli/nbs/netrun_cli/06_graph.ipynb create mode 100644 netrun-cli/nbs/tests/test_cli.ipynb create mode 100644 netrun-cli/nbs/tests/test_cli_graph.ipynb rename {netrun/pts/netrun/10_cli => netrun-cli/pts/netrun_cli}/00_app.pct.py (80%) rename {netrun/pts/netrun/10_cli => netrun-cli/pts/netrun_cli}/01_helpers.pct.py (99%) rename {netrun/pts/netrun/10_cli => netrun-cli/pts/netrun_cli}/02_commands.pct.py (73%) rename {netrun/pts/netrun/10_cli => netrun-cli/pts/netrun_cli}/03_actions.pct.py (97%) rename {netrun/pts/netrun/10_cli => netrun-cli/pts/netrun_cli}/04_recipes.pct.py (97%) rename {netrun/pts/netrun/10_cli => netrun-cli/pts/netrun_cli}/05_download_agents.pct.py (98%) rename {netrun/pts/netrun/10_cli => netrun-cli/pts/netrun_cli}/06_graph.pct.py (99%) rename {netrun/pts/tests/10_cli => netrun-cli/pts/tests}/__init__.py (100%) rename {netrun/pts/tests/10_cli => netrun-cli/pts/tests}/test_cli.pct.py (70%) rename {netrun/pts/tests/10_cli => netrun-cli/pts/tests}/test_cli_graph.pct.py (99%) create mode 100644 netrun-cli/pyproject.toml create mode 100644 netrun-cli/src/netrun_cli/__init__.py rename {netrun/src/netrun/cli => netrun-cli/src/netrun_cli}/_actions.py (89%) rename {netrun/src/netrun/cli => netrun-cli/src/netrun_cli}/_app.py (60%) rename {netrun/src/netrun/cli => netrun-cli/src/netrun_cli}/_commands.py (69%) rename {netrun/src/netrun/cli => netrun-cli/src/netrun_cli}/_download_agents.py (92%) rename {netrun/src/netrun/cli => netrun-cli/src/netrun_cli}/_graph.py (97%) rename {netrun/src/netrun/cli => netrun-cli/src/netrun_cli}/_helpers.py (95%) rename {netrun/src/netrun/cli => netrun-cli/src/netrun_cli}/_recipes.py (88%) rename {netrun/src/tests/cli => netrun-cli/src/tests}/test_cli.py (66%) rename {netrun/src/tests/cli => netrun-cli/src/tests}/test_cli_graph.py (95%) create mode 100644 netrun-cli/uv.lock diff --git a/netrun-cli/nblite.toml b/netrun-cli/nblite.toml new file mode 100644 index 00000000..453a201a --- /dev/null +++ b/netrun-cli/nblite.toml @@ -0,0 +1,31 @@ +export_pipeline = """ +nbs -> pts +pts -> lib + +nbs_tests -> pts_tests +pts_tests -> lib_tests +""" + +[cl.lib] +path="src/netrun_cli" +format="module" + +[cl.nbs] +path="nbs/netrun_cli" +format="ipynb" + +[cl.pts] +path="pts/netrun_cli" +format="percent" + +[cl.lib_tests] +path="src/tests" +format="module" + +[cl.nbs_tests] +path="nbs/tests" +format="ipynb" + +[cl.pts_tests] +path="pts/tests" +format="percent" diff --git a/netrun-cli/nbs/netrun_cli/00_app.ipynb b/netrun-cli/nbs/netrun_cli/00_app.ipynb new file mode 100644 index 00000000..d533963c --- /dev/null +++ b/netrun-cli/nbs/netrun_cli/00_app.ipynb @@ -0,0 +1,147 @@ +{ + "cells": [ + { + "cell_type": "code", + "source": [ + "#|default_exp _app" + ], + "execution_count": null, + "outputs": [], + "metadata": {} + }, + { + "cell_type": "code", + "source": [ + "#|hide\n", + "from nblite import nbl_export; nbl_export();" + ], + "execution_count": null, + "outputs": [], + "metadata": {} + }, + { + "cell_type": "code", + "source": [ + "#|export\n", + "import typer" + ], + "execution_count": null, + "outputs": [], + "metadata": {} + }, + { + "cell_type": "markdown", + "source": [ + "# CLI Application\n", + "\n", + "Typer-based CLI for inspecting and managing netrun projects." + ], + "metadata": {} + }, + { + "cell_type": "code", + "source": [ + "#|export\n", + "app = typer.Typer(\n", + " name=\"netrun\",\n", + " help=\"CLI for inspecting and managing netrun projects.\",\n", + " no_args_is_help=True,\n", + ")" + ], + "execution_count": null, + "outputs": [], + "metadata": {} + }, + { + "cell_type": "code", + "source": [ + "#|export\n", + "from netrun_cli._commands import (\n", + " validate,\n", + " structure,\n", + " convert,\n", + " factory_info,\n", + " info,\n", + " nodes,\n", + " node,\n", + " dry_run,\n", + ")\n", + "\n", + "app.command(\"validate\")(validate)\n", + "app.command(\"structure\")(structure)\n", + "app.command(\"convert\")(convert)\n", + "app.command(\"factory-info\")(factory_info)\n", + "app.command(\"info\")(info)\n", + "app.command(\"nodes\")(nodes)\n", + "app.command(\"node\")(node)\n", + "app.command(\"dry-run\")(dry_run)" + ], + "execution_count": null, + "outputs": [], + "metadata": {} + }, + { + "cell_type": "code", + "source": [ + "#|export\n", + "from netrun_cli._download_agents import download_agents\n", + "\n", + "app.command(\"download-agents\")(download_agents)" + ], + "execution_count": null, + "outputs": [], + "metadata": {} + }, + { + "cell_type": "code", + "source": [ + "#|export\n", + "from netrun_cli._graph import add_node, remove_node, edit_node, add_edge, remove_edge\n", + "\n", + "app.command(\"add-node\")(add_node)\n", + "app.command(\"remove-node\")(remove_node)\n", + "app.command(\"edit-node\")(edit_node)\n", + "app.command(\"add-edge\")(add_edge)\n", + "app.command(\"remove-edge\")(remove_edge)" + ], + "execution_count": null, + "outputs": [], + "metadata": {} + }, + { + "cell_type": "code", + "source": [ + "#|export\n", + "from netrun_cli._actions import actions_app\n", + "from netrun_cli._recipes import recipes_app\n", + "\n", + "app.add_typer(actions_app, name=\"actions\")\n", + "app.add_typer(recipes_app, name=\"recipes\")" + ], + "execution_count": null, + "outputs": [], + "metadata": {} + }, + { + "cell_type": "code", + "source": [ + "#|export\n", + "def app_main() -> None:\n", + " \"\"\"Entry point for the netrun CLI.\"\"\"\n", + " app()" + ], + "execution_count": null, + "outputs": [], + "metadata": {} + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/netrun-cli/nbs/netrun_cli/01_helpers.ipynb b/netrun-cli/nbs/netrun_cli/01_helpers.ipynb new file mode 100644 index 00000000..7adc806d --- /dev/null +++ b/netrun-cli/nbs/netrun_cli/01_helpers.ipynb @@ -0,0 +1,293 @@ +{ + "cells": [ + { + "cell_type": "code", + "source": [ + "#|default_exp _helpers" + ], + "execution_count": null, + "outputs": [], + "metadata": {} + }, + { + "cell_type": "code", + "source": [ + "#|hide\n", + "from nblite import nbl_export; nbl_export();" + ], + "execution_count": null, + "outputs": [], + "metadata": {} + }, + { + "cell_type": "code", + "source": [ + "#|export\n", + "import json\n", + "import sys\n", + "import tomllib\n", + "from pathlib import Path\n", + "from typing import Annotated, Any, Optional\n", + "\n", + "import typer\n", + "\n", + "from netrun.net.config._net_config import NetConfig\n", + "from netrun.net.config._nodes import NodeConfig" + ], + "execution_count": null, + "outputs": [], + "metadata": {} + }, + { + "cell_type": "markdown", + "source": [ + "# CLI Helpers\n", + "\n", + "Shared utilities for the netrun CLI commands." + ], + "metadata": {} + }, + { + "cell_type": "code", + "source": [ + "#|export\n", + "ConfigOpt = Annotated[Optional[str], typer.Option(\"--config\", \"-c\", help=\"Path to netrun config file.\")]\n", + "PrettyOpt = Annotated[bool, typer.Option(\"--pretty/--compact\", help=\"Pretty-print or compact JSON output.\")]" + ], + "execution_count": null, + "outputs": [], + "metadata": {} + }, + { + "cell_type": "code", + "source": [ + "#|export\n", + "NETRUN_EXTENSIONS = (\".netrun.json\", \".netrun.toml\")\n", + "\n", + "\n", + "def find_config(path: str | None) -> Path:\n", + " \"\"\"Find a netrun config file.\n", + "\n", + " If *path* is given, resolve and return it.\n", + " Otherwise search the current directory for files ending with\n", + " ``*.netrun.json`` or ``*.netrun.toml``.\n", + "\n", + " Raises:\n", + " typer.Exit: If no config found, multiple found, or path doesn't exist.\n", + " \"\"\"\n", + " if path is not None:\n", + " p = Path(path).resolve()\n", + " if not p.exists():\n", + " typer.echo(f\"Error: config file not found: {p}\", err=True)\n", + " raise typer.Exit(1)\n", + " return p\n", + "\n", + " cwd = Path.cwd()\n", + " matches = [\n", + " f for f in cwd.iterdir()\n", + " if f.is_file() and any(f.name.endswith(ext) for ext in NETRUN_EXTENSIONS)\n", + " ]\n", + "\n", + " if len(matches) == 0:\n", + " typer.echo(\"Error: no *netrun.json or *netrun.toml found in current directory.\", err=True)\n", + " raise typer.Exit(1)\n", + " if len(matches) > 1:\n", + " names = \", \".join(f.name for f in matches)\n", + " typer.echo(f\"Error: multiple config files found: {names}. Use --config to specify one.\", err=True)\n", + " raise typer.Exit(1)\n", + "\n", + " return matches[0].resolve()\n", + "\n", + "\n", + "def load_config(path: str | None) -> tuple[NetConfig, Path]:\n", + " \"\"\"Find and load a ``NetConfig`` from a file.\n", + "\n", + " Returns:\n", + " Tuple of (config, resolved_file_path).\n", + " \"\"\"\n", + " config_path = find_config(path)\n", + " try:\n", + " config = NetConfig.from_file(config_path)\n", + " except Exception as e:\n", + " typer.echo(f\"Error loading config: {e}\", err=True)\n", + " raise typer.Exit(1)\n", + " return config, config_path\n", + "\n", + "\n", + "def load_resolved_config(path: str | None) -> tuple[NetConfig, Path]:\n", + " \"\"\"Load and resolve a ``NetConfig`` (expanding factories).\n", + "\n", + " Temporarily adds ``project_root_path`` to ``sys.path`` so that\n", + " dotted import paths relative to the project root (e.g.\n", + " ``\"nodes.double\"``) can be resolved.\n", + "\n", + " Falls back to unresolved config if resolution still fails.\n", + "\n", + " Returns:\n", + " Tuple of (config, resolved_file_path).\n", + " \"\"\"\n", + " config, config_path = load_config(path)\n", + " project_root = str(config.project_root_path)\n", + " added = False\n", + " if project_root not in sys.path:\n", + " sys.path.insert(0, project_root)\n", + " added = True\n", + " try:\n", + " config = config.resolve()\n", + " except Exception as e:\n", + " typer.echo(\n", + " f\"Warning: could not resolve config (factories may not be importable): {e}\",\n", + " err=True,\n", + " )\n", + " finally:\n", + " if added:\n", + " sys.path.remove(project_root)\n", + " return config, config_path\n", + "\n", + "\n", + "def load_raw_data(path: str | None) -> tuple[dict[str, Any], Path]:\n", + " \"\"\"Find and load a config file as a raw dict.\n", + "\n", + " Needed for accessing top-level keys (like ``recipes``) that are\n", + " not part of the ``NetConfig`` pydantic model.\n", + "\n", + " Returns:\n", + " Tuple of (raw_dict, resolved_file_path).\n", + " \"\"\"\n", + " config_path = find_config(path)\n", + " try:\n", + " content = config_path.read_text()\n", + " suffix = config_path.suffix.lower()\n", + " if suffix == \".json\":\n", + " data = json.loads(content)\n", + " elif suffix == \".toml\":\n", + " data = tomllib.loads(content)\n", + " else:\n", + " typer.echo(f\"Error: unsupported format {suffix}\", err=True)\n", + " raise typer.Exit(1)\n", + " except (json.JSONDecodeError, tomllib.TOMLDecodeError) as e:\n", + " typer.echo(f\"Error parsing config: {e}\", err=True)\n", + " raise typer.Exit(1)\n", + " return data, config_path\n", + "\n", + "\n", + "def output_json(data: Any, pretty: bool) -> None:\n", + " \"\"\"Print *data* as JSON to stdout.\"\"\"\n", + " indent = 2 if pretty else None\n", + " typer.echo(json.dumps(data, indent=indent, default=str))\n", + "\n", + "\n", + "def get_node_by_name(config: NetConfig, name: str) -> NodeConfig:\n", + " \"\"\"Look up a node by name, exiting with code 1 if not found.\"\"\"\n", + " for n in config.graph.nodes:\n", + " if n.name == name:\n", + " if isinstance(n, NodeConfig):\n", + " return n\n", + " typer.echo(f\"Error: node '{name}' not found.\", err=True)\n", + " raise typer.Exit(1)\n", + "\n", + "\n", + "def port_type_str(port_type: Any) -> str | None:\n", + " \"\"\"Convert a ``PortTypeSpec`` to a human-readable string.\"\"\"\n", + " if port_type is None:\n", + " return None\n", + " if isinstance(port_type, str):\n", + " return port_type\n", + " if isinstance(port_type, type):\n", + " return port_type.__name__\n", + " # PortTypeConfig\n", + " if hasattr(port_type, \"name\"):\n", + " return port_type.name\n", + " return str(port_type)" + ], + "execution_count": null, + "outputs": [], + "metadata": {} + }, + { + "cell_type": "code", + "source": [ + "#|export\n", + "def write_config_data(data: dict, path: Path) -> None:\n", + " \"\"\"Write a raw config dict back to a file (.json or .toml).\n", + "\n", + " Warns on stderr when writing TOML (comments are lost on round-trip).\n", + " \"\"\"\n", + " suffix = path.suffix.lower()\n", + " if suffix == \".json\":\n", + " path.write_text(json.dumps(data, indent=2, default=str) + \"\\n\")\n", + " elif suffix == \".toml\":\n", + " try:\n", + " import tomli_w\n", + " except ImportError:\n", + " typer.echo(\"Error: tomli-w is required for TOML output. Install with: pip install tomli-w\", err=True)\n", + " raise typer.Exit(1)\n", + " typer.echo(\"Warning: writing TOML — any comments in the original file will be lost.\", err=True)\n", + " path.write_text(tomli_w.dumps(data))\n", + " else:\n", + " typer.echo(f\"Error: unsupported config format '{suffix}'\", err=True)\n", + " raise typer.Exit(1)\n", + "\n", + "\n", + "def deep_merge(base: dict, override: dict) -> dict:\n", + " \"\"\"Recursively merge *override* into *base*, returning a new dict.\n", + "\n", + " If both values for a key are dicts, recurse. Otherwise *override* wins.\n", + " Lists are replaced, not merged.\n", + " \"\"\"\n", + " result = dict(base)\n", + " for key, value in override.items():\n", + " if key in result and isinstance(result[key], dict) and isinstance(value, dict):\n", + " result[key] = deep_merge(result[key], value)\n", + " else:\n", + " result[key] = value\n", + " return result\n", + "\n", + "\n", + "def auto_position(nodes: list[dict]) -> dict:\n", + " \"\"\"Compute a reasonable UI position for a new node.\n", + "\n", + " Scans existing ``extra.ui.position.x`` values and places the new node\n", + " 300px to the right of the rightmost node at the average y.\n", + " \"\"\"\n", + " xs: list[float] = []\n", + " ys: list[float] = []\n", + " for n in nodes:\n", + " pos = n.get(\"extra\", {}).get(\"ui\", {}).get(\"position\", {})\n", + " if \"x\" in pos:\n", + " xs.append(float(pos[\"x\"]))\n", + " if \"y\" in pos:\n", + " ys.append(float(pos[\"y\"]))\n", + " if xs:\n", + " return {\"x\": max(xs) + 300, \"y\": sum(ys) / len(ys) if ys else 150}\n", + " return {\"x\": 100, \"y\": 150}\n", + "\n", + "\n", + "def validate_after_write(path: Path) -> None:\n", + " \"\"\"Run validation on a config file after writing, printing warnings to stderr.\n", + "\n", + " Validation issues are informational only — the write already succeeded.\n", + " \"\"\"\n", + " try:\n", + " config = NetConfig.from_file(path)\n", + " errors = config.graph.validate()\n", + " for err in errors:\n", + " typer.echo(f\"Warning: {err.type}: {err.msg}\", err=True)\n", + " except Exception:\n", + " pass # File was already written; skip if validation itself fails" + ], + "execution_count": null, + "outputs": [], + "metadata": {} + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/netrun-cli/nbs/netrun_cli/02_commands.ipynb b/netrun-cli/nbs/netrun_cli/02_commands.ipynb new file mode 100644 index 00000000..c2ca88e5 --- /dev/null +++ b/netrun-cli/nbs/netrun_cli/02_commands.ipynb @@ -0,0 +1,654 @@ +{ + "cells": [ + { + "cell_type": "code", + "source": [ + "#|default_exp _commands" + ], + "execution_count": null, + "outputs": [], + "metadata": {} + }, + { + "cell_type": "code", + "source": [ + "#|hide\n", + "from nblite import nbl_export; nbl_export();" + ], + "execution_count": null, + "outputs": [], + "metadata": {} + }, + { + "cell_type": "code", + "source": [ + "#|export\n", + "import importlib\n", + "import inspect\n", + "import json\n", + "import sys\n", + "import tomllib\n", + "from pathlib import Path\n", + "from typing import Annotated, Any, Optional\n", + "\n", + "import typer\n", + "from pydantic import ValidationError\n", + "\n", + "from netrun_cli._helpers import (\n", + " ConfigOpt,\n", + " PrettyOpt,\n", + " find_config,\n", + " load_config,\n", + " load_resolved_config,\n", + " load_raw_data,\n", + " output_json,\n", + " get_node_by_name,\n", + " port_type_str,\n", + ")\n", + "from netrun.net.config._nodes import NodeConfig, SubgraphConfig" + ], + "execution_count": null, + "outputs": [], + "metadata": {} + }, + { + "cell_type": "markdown", + "source": [ + "# Core CLI Commands" + ], + "metadata": {} + }, + { + "cell_type": "markdown", + "source": [ + "## validate" + ], + "metadata": {} + }, + { + "cell_type": "code", + "source": [ + "#|export\n", + "def validate(\n", + " config: ConfigOpt = None,\n", + " pretty: PrettyOpt = True,\n", + ") -> None:\n", + " \"\"\"Validate a netrun config file.\"\"\"\n", + " config_path = find_config(config)\n", + " errors: list[str] = []\n", + " warnings: list[str] = []\n", + " node_count = 0\n", + " edge_count = 0\n", + "\n", + " # Step 1: Pydantic validation — must succeed to continue\n", + " try:\n", + " from netrun.net.config._net_config import NetConfig\n", + " net_config = NetConfig.from_file(config_path)\n", + " node_count = len(net_config.graph.nodes)\n", + " edge_count = len(net_config.graph.edges)\n", + " except ValidationError as e:\n", + " for err in e.errors():\n", + " loc = \" → \".join(str(l) for l in err[\"loc\"])\n", + " errors.append({\"loc\": loc, \"msg\": err[\"msg\"], \"type\": err[\"type\"]})\n", + " output_json({\"valid\": False, \"file\": str(config_path), \"errors\": errors}, pretty)\n", + " raise typer.Exit(1)\n", + " except Exception as e:\n", + " errors.append(f\"Config validation error: {e}\")\n", + " output_json({\"valid\": False, \"file\": str(config_path), \"errors\": errors}, pretty)\n", + " raise typer.Exit(1)\n", + "\n", + " # Step 2: Pre-resolution structural validation (always runs)\n", + " config_errors = net_config.graph.validate()\n", + " for err in config_errors:\n", + " errors.append(f\"{err.type}: {err.msg}\")\n", + "\n", + " # Step 3: Post-resolution validation via Rust (only if resolve succeeds)\n", + " try:\n", + " resolved = net_config.resolve()\n", + " except Exception as e:\n", + " resolved = None\n", + " warnings.append(f\"Resolution error: {e}\")\n", + "\n", + " if resolved is not None:\n", + " try:\n", + " resolved.graph.get_graph() # validates via netrun_sim\n", + " except ValueError as e:\n", + " errors.append(f\"Graph validation error: {e}\")\n", + "\n", + " # Step 4: Raw data checks (recipes, actions) — always runs\n", + " try:\n", + " raw, _ = load_raw_data(config)\n", + " config_dir = config_path.parent\n", + "\n", + " # Check recipe file paths exist\n", + " raw_recipes = raw.get(\"recipes\", {})\n", + " for name, recipe in raw_recipes.items():\n", + " recipe_path = recipe.get(\"path\")\n", + " if recipe_path:\n", + " rp = Path(recipe_path)\n", + " if not rp.is_absolute():\n", + " rp = config_dir / rp\n", + " if not rp.exists():\n", + " errors.append(f\"Recipe '{name}': file not found: {rp}\")\n", + "\n", + " # Check actions have required fields\n", + " def _check_actions(actions: list[dict], prefix: str) -> None:\n", + " for i, action in enumerate(actions):\n", + " for field in (\"id\", \"label\", \"command\"):\n", + " if field not in action:\n", + " errors.append(f\"{prefix} action [{i}]: missing '{field}'\")\n", + "\n", + " graph_extra = raw.get(\"graph\", {}).get(\"extra\", raw.get(\"extra\", {}))\n", + " ui = graph_extra.get(\"ui\", {})\n", + " _check_actions(ui.get(\"actions\", []), \"Project\")\n", + "\n", + " # Check per-node actions\n", + " for node_data in raw.get(\"graph\", {}).get(\"nodes\", []):\n", + " node_name = node_data.get(\"name\", \"?\")\n", + " node_ui = node_data.get(\"extra\", {}).get(\"ui\", {})\n", + " _check_actions(node_ui.get(\"actions\", []), f\"Node '{node_name}'\")\n", + "\n", + " except Exception as e:\n", + " errors.append(f\"Raw data check error: {e}\")\n", + "\n", + " if errors:\n", + " result = {\"valid\": False, \"file\": str(config_path), \"errors\": errors}\n", + " if warnings:\n", + " result[\"warnings\"] = warnings\n", + " output_json(result, pretty)\n", + " raise typer.Exit(1)\n", + " else:\n", + " result = {\"valid\": True, \"file\": str(config_path), \"nodes\": node_count, \"edges\": edge_count}\n", + " if warnings:\n", + " result[\"warnings\"] = warnings\n", + " output_json(result, pretty)" + ], + "execution_count": null, + "outputs": [], + "metadata": {} + }, + { + "cell_type": "markdown", + "source": [ + "## structure" + ], + "metadata": {} + }, + { + "cell_type": "code", + "source": [ + "#|export\n", + "def _port_map(ports: dict) -> dict[str, str | None]:\n", + " \"\"\"Convert port dict to {name: type_str}.\"\"\"\n", + " return {name: port_type_str(pc.port_type) for name, pc in ports.items()}\n", + "\n", + "\n", + "def _mermaid_id(name: str) -> str:\n", + " \"\"\"Sanitise a node name for use as a Mermaid node ID.\"\"\"\n", + " return name.replace(\" \", \"_\").replace(\"-\", \"_\")\n", + "\n", + "\n", + "def structure(\n", + " config: ConfigOpt = None,\n", + " format: Annotated[str, typer.Option(\"--format\", \"-f\", help=\"Output format: json or mermaid.\")] = \"json\",\n", + " pretty: PrettyOpt = True,\n", + ") -> None:\n", + " \"\"\"Output graph topology (default JSON; use --format mermaid for Mermaid diagram).\"\"\"\n", + " net_config, config_path = load_resolved_config(config)\n", + "\n", + " if format == \"mermaid\":\n", + " lines = [\"graph LR\"]\n", + " for n in net_config.graph.nodes:\n", + " mid = _mermaid_id(n.name)\n", + " lines.append(f' {mid}[\"{n.name}\"]')\n", + " for e in net_config.graph.edges:\n", + " src = _mermaid_id(e.source_node)\n", + " tgt = _mermaid_id(e.target_node)\n", + " label = f\"{e.source_port} → {e.target_port}\"\n", + " lines.append(f' {src} -->|\"{label}\"| {tgt}')\n", + " typer.echo(\"\\n\".join(lines))\n", + " return\n", + "\n", + " if format != \"json\":\n", + " typer.echo(f\"Error: unsupported format '{format}'. Use 'json' or 'mermaid'.\", err=True)\n", + " raise typer.Exit(1)\n", + "\n", + " nodes_out = []\n", + " for n in net_config.graph.nodes:\n", + " if isinstance(n, SubgraphConfig):\n", + " nodes_out.append({\n", + " \"name\": n.name,\n", + " \"type\": \"subgraph\",\n", + " })\n", + " continue\n", + " entry: dict[str, Any] = {\n", + " \"name\": n.name,\n", + " \"in_ports\": _port_map(n.in_ports),\n", + " \"out_ports\": _port_map(n.out_ports),\n", + " }\n", + " if n.in_salvo_conditions:\n", + " entry[\"in_salvo_conditions\"] = list(n.in_salvo_conditions.keys())\n", + " if n.out_salvo_conditions:\n", + " entry[\"out_salvo_conditions\"] = list(n.out_salvo_conditions.keys())\n", + " if n.factory:\n", + " entry[\"factory\"] = str(n.factory) if not isinstance(n.factory, str) else n.factory\n", + " if n.factory_args:\n", + " entry[\"factory_args\"] = n.factory_args\n", + " nodes_out.append(entry)\n", + "\n", + " edges_out = []\n", + " for e in net_config.graph.edges:\n", + " edges_out.append({\n", + " \"source\": f\"{e.source_node}.{e.source_port}\",\n", + " \"target\": f\"{e.target_node}.{e.target_port}\",\n", + " })\n", + "\n", + " output_json({\"nodes\": nodes_out, \"edges\": edges_out}, pretty)" + ], + "execution_count": null, + "outputs": [], + "metadata": {} + }, + { + "cell_type": "markdown", + "source": [ + "## convert" + ], + "metadata": {} + }, + { + "cell_type": "code", + "source": [ + "#|export\n", + "def convert(\n", + " config_file: Annotated[str, typer.Argument(help=\"Path to config file to convert.\")],\n", + " output: Annotated[Optional[str], typer.Option(\"--output\", \"-o\", help=\"Output file path.\")] = None,\n", + " pretty: Annotated[bool, typer.Option(\"--pretty/--compact\", help=\"Pretty or compact output.\")] = True,\n", + ") -> None:\n", + " \"\"\"Convert between .netrun.json and .netrun.toml formats.\"\"\"\n", + " src = Path(config_file).resolve()\n", + " if not src.exists():\n", + " typer.echo(f\"Error: file not found: {src}\", err=True)\n", + " raise typer.Exit(1)\n", + "\n", + " content = src.read_text()\n", + " src_name = src.name.lower()\n", + "\n", + " if src_name.endswith(\".netrun.json\"):\n", + " # JSON -> TOML\n", + " data = json.loads(content)\n", + " try:\n", + " import tomli_w\n", + " except ImportError:\n", + " typer.echo(\"Error: tomli-w is required for TOML output. Install with: pip install tomli-w\", err=True)\n", + " raise typer.Exit(1)\n", + " result = tomli_w.dumps(data)\n", + " default_ext = \".netrun.toml\"\n", + " elif src_name.endswith(\".netrun.toml\"):\n", + " # TOML -> JSON\n", + " data = tomllib.loads(content)\n", + " indent = 2 if pretty else None\n", + " result = json.dumps(data, indent=indent, default=str)\n", + " default_ext = \".netrun.json\"\n", + " else:\n", + " typer.echo(\"Error: file must end with .netrun.json or .netrun.toml\", err=True)\n", + " raise typer.Exit(1)\n", + "\n", + " if output:\n", + " Path(output).write_text(result)\n", + " typer.echo(f\"Written to {output}\")\n", + " else:\n", + " typer.echo(result)" + ], + "execution_count": null, + "outputs": [], + "metadata": {} + }, + { + "cell_type": "markdown", + "source": [ + "## factory-info" + ], + "metadata": {} + }, + { + "cell_type": "code", + "source": [ + "#|export\n", + "def factory_info(\n", + " factory_path: Annotated[str, typer.Argument(help=\"Dotted import path to factory module.\")],\n", + " pretty: PrettyOpt = True,\n", + ") -> None:\n", + " \"\"\"Inspect a factory module: parameters, types, defaults.\"\"\"\n", + " try:\n", + " module = importlib.import_module(factory_path)\n", + " except ImportError as e:\n", + " typer.echo(f\"Error: cannot import '{factory_path}': {e}\", err=True)\n", + " raise typer.Exit(1)\n", + "\n", + " get_node_config_fn = getattr(module, \"get_node_config\", None)\n", + " has_get_node_funcs = hasattr(module, \"get_node_funcs\")\n", + "\n", + " if get_node_config_fn is None:\n", + " typer.echo(f\"Error: '{factory_path}' has no get_node_config function.\", err=True)\n", + " raise typer.Exit(1)\n", + "\n", + " sig = inspect.signature(get_node_config_fn)\n", + " params = []\n", + " for name, param in sig.parameters.items():\n", + " # Skip internal parameters (e.g. _net_config) injected by the system\n", + " if name.startswith(\"_\"):\n", + " continue\n", + " p: dict[str, Any] = {\"name\": name}\n", + " if param.annotation != inspect.Parameter.empty:\n", + " p[\"type\"] = str(param.annotation)\n", + " if param.default != inspect.Parameter.empty:\n", + " p[\"default\"] = repr(param.default)\n", + " p[\"required\"] = False\n", + " else:\n", + " p[\"required\"] = True\n", + " params.append(p)\n", + "\n", + " result: dict[str, Any] = {\n", + " \"factory\": factory_path,\n", + " \"type\": \"node\" if has_get_node_funcs else \"subgraph\",\n", + " \"params\": params,\n", + " }\n", + "\n", + " desc = getattr(module, \"_factory_desc\", None)\n", + " if desc:\n", + " result[\"description\"] = desc\n", + "\n", + " output_json(result, pretty)" + ], + "execution_count": null, + "outputs": [], + "metadata": {} + }, + { + "cell_type": "markdown", + "source": [ + "## info" + ], + "metadata": {} + }, + { + "cell_type": "code", + "source": [ + "#|export\n", + "def info(\n", + " config: ConfigOpt = None,\n", + " pretty: PrettyOpt = True,\n", + ") -> None:\n", + " \"\"\"Summary stats for a netrun config.\"\"\"\n", + " net_config, config_path = load_resolved_config(config)\n", + " raw, _ = load_raw_data(config)\n", + "\n", + " node_count = 0\n", + " subgraph_count = 0\n", + " factories_used: list[str] = []\n", + " for n in net_config.graph.nodes:\n", + " if isinstance(n, SubgraphConfig):\n", + " subgraph_count += 1\n", + " else:\n", + " node_count += 1\n", + " if n.factory:\n", + " f = str(n.factory) if not isinstance(n.factory, str) else n.factory\n", + " if f not in factories_used:\n", + " factories_used.append(f)\n", + "\n", + " edge_count = len(net_config.graph.edges)\n", + "\n", + " # Pool info\n", + " pool_info: dict[str, str] = {}\n", + " if net_config.pools:\n", + " for pname, pcfg in net_config.pools.items():\n", + " pool_info[pname] = pcfg.spec.type\n", + "\n", + " # Action count\n", + " graph_extra = net_config.extra\n", + " ui = graph_extra.get(\"ui\", {})\n", + " action_count = len(ui.get(\"actions\", []))\n", + "\n", + " # Recipe count\n", + " recipe_count = len(raw.get(\"recipes\", {}))\n", + "\n", + " result: dict[str, Any] = {\n", + " \"file\": str(config_path),\n", + " \"project_root\": str(net_config.project_root_path),\n", + " \"nodes\": node_count,\n", + " \"edges\": edge_count,\n", + " }\n", + " if subgraph_count:\n", + " result[\"subgraphs\"] = subgraph_count\n", + " if pool_info:\n", + " result[\"pools\"] = pool_info\n", + " if factories_used:\n", + " result[\"factories\"] = factories_used\n", + " result[\"actions\"] = action_count\n", + " result[\"recipes\"] = recipe_count\n", + "\n", + " output_json(result, pretty)" + ], + "execution_count": null, + "outputs": [], + "metadata": {} + }, + { + "cell_type": "markdown", + "source": [ + "## nodes" + ], + "metadata": {} + }, + { + "cell_type": "code", + "source": [ + "#|export\n", + "def nodes(\n", + " config: ConfigOpt = None,\n", + " pretty: PrettyOpt = True,\n", + ") -> None:\n", + " \"\"\"List all nodes with port names.\"\"\"\n", + " net_config, _ = load_resolved_config(config)\n", + "\n", + " result = []\n", + " for n in net_config.graph.nodes:\n", + " if isinstance(n, SubgraphConfig):\n", + " result.append({\"name\": n.name, \"type\": \"subgraph\"})\n", + " continue\n", + " entry: dict[str, Any] = {\n", + " \"name\": n.name,\n", + " \"in_ports\": list(n.in_ports.keys()),\n", + " \"out_ports\": list(n.out_ports.keys()),\n", + " }\n", + " if n.factory:\n", + " entry[\"factory\"] = str(n.factory) if not isinstance(n.factory, str) else n.factory\n", + " result.append(entry)\n", + "\n", + " output_json(result, pretty)" + ], + "execution_count": null, + "outputs": [], + "metadata": {} + }, + { + "cell_type": "markdown", + "source": [ + "## node" + ], + "metadata": {} + }, + { + "cell_type": "code", + "source": [ + "#|export\n", + "def node(\n", + " name: Annotated[str, typer.Argument(help=\"Node name.\")],\n", + " config: ConfigOpt = None,\n", + " edges: Annotated[bool, typer.Option(\"--edges\", help=\"Include connected edges.\")] = False,\n", + " pretty: PrettyOpt = True,\n", + ") -> None:\n", + " \"\"\"Detailed info about a specific node.\"\"\"\n", + " net_config, _ = load_resolved_config(config)\n", + " n = get_node_by_name(net_config, name)\n", + "\n", + " result: dict[str, Any] = {\n", + " \"name\": n.name,\n", + " \"in_ports\": {pname: {\"type\": port_type_str(pc.port_type)} for pname, pc in n.in_ports.items()},\n", + " \"out_ports\": {pname: {\"type\": port_type_str(pc.port_type)} for pname, pc in n.out_ports.items()},\n", + " }\n", + "\n", + " if n.in_salvo_conditions:\n", + " result[\"in_salvo_conditions\"] = list(n.in_salvo_conditions.keys())\n", + " if n.out_salvo_conditions:\n", + " result[\"out_salvo_conditions\"] = list(n.out_salvo_conditions.keys())\n", + " if n.factory:\n", + " result[\"factory\"] = str(n.factory) if not isinstance(n.factory, str) else n.factory\n", + " if n.factory_args:\n", + " result[\"factory_args\"] = n.factory_args\n", + " if n.execution_config:\n", + " ec = n.execution_config\n", + " result[\"execution_config\"] = {\n", + " \"pools\": ec.pools,\n", + " \"type_checking_enabled\": ec.type_checking_enabled,\n", + " \"retries\": ec.retries,\n", + " \"timeout\": ec.timeout,\n", + " }\n", + " if n.extra:\n", + " result[\"extra\"] = n.extra\n", + "\n", + " if edges:\n", + " incoming = []\n", + " outgoing = []\n", + " for e in net_config.graph.edges:\n", + " if e.target_node == name:\n", + " incoming.append({\"source\": f\"{e.source_node}.{e.source_port}\", \"port\": e.target_port})\n", + " if e.source_node == name:\n", + " outgoing.append({\"port\": e.source_port, \"target\": f\"{e.target_node}.{e.target_port}\"})\n", + " result[\"edges\"] = {\"incoming\": incoming, \"outgoing\": outgoing}\n", + "\n", + " output_json(result, pretty)" + ], + "execution_count": null, + "outputs": [], + "metadata": {} + }, + { + "cell_type": "markdown", + "source": [ + "## dry-run" + ], + "metadata": {} + }, + { + "cell_type": "code", + "source": [ + "#|export\n", + "def _topological_sort(nodes: list[str], edges: list[tuple[str, str]]) -> list[str]:\n", + " \"\"\"Kahn's algorithm for topological sort. Returns nodes in execution order.\"\"\"\n", + " from collections import defaultdict, deque\n", + "\n", + " in_degree: dict[str, int] = {n: 0 for n in nodes}\n", + " adjacency: dict[str, list[str]] = defaultdict(list)\n", + "\n", + " for src, tgt in edges:\n", + " if src in in_degree and tgt in in_degree:\n", + " adjacency[src].append(tgt)\n", + " in_degree[tgt] += 1\n", + "\n", + " queue = deque(n for n in nodes if in_degree[n] == 0)\n", + " result = []\n", + "\n", + " while queue:\n", + " n = queue.popleft()\n", + " result.append(n)\n", + " for neighbor in adjacency[n]:\n", + " in_degree[neighbor] -= 1\n", + " if in_degree[neighbor] == 0:\n", + " queue.append(neighbor)\n", + "\n", + " return result\n", + "\n", + "\n", + "def dry_run(\n", + " config: ConfigOpt = None,\n", + " pretty: PrettyOpt = True,\n", + ") -> None:\n", + " \"\"\"Show execution plan without running node code.\n", + "\n", + " Displays topological order, source/sink nodes, pools, and scheduling constraints.\n", + " \"\"\"\n", + " net_config, _ = load_resolved_config(config)\n", + "\n", + " node_names = [n.name for n in net_config.graph.nodes]\n", + "\n", + " # Build data edges (non-dependency) for topological ordering\n", + " data_edges = [\n", + " (e.source_node, e.target_node)\n", + " for e in net_config.graph.edges\n", + " if not e.dependency\n", + " ]\n", + "\n", + " # Include depends_on as edges for ordering\n", + " for n in net_config.graph.nodes:\n", + " if isinstance(n, SubgraphConfig):\n", + " continue\n", + " if n.execution_config and n.execution_config.depends_on:\n", + " for dep in n.execution_config.depends_on:\n", + " data_edges.append((dep, n.name))\n", + "\n", + " topo_order = _topological_sort(node_names, data_edges)\n", + "\n", + " # Source nodes: no incoming data edges\n", + " targets = {tgt for _, tgt in data_edges}\n", + " source_nodes = [n for n in topo_order if n not in targets]\n", + "\n", + " # Sink nodes: no outgoing data edges\n", + " sources = {src for src, _ in data_edges}\n", + " sink_nodes = [n for n in topo_order if n not in sources]\n", + "\n", + " # Build execution order with metadata\n", + " execution_order = []\n", + " for name in topo_order:\n", + " entry: dict[str, Any] = {\"node\": name}\n", + "\n", + " for n in net_config.graph.nodes:\n", + " if n.name == name and not isinstance(n, SubgraphConfig):\n", + " if n.execution_config:\n", + " ec = n.execution_config\n", + " if ec.pools:\n", + " entry[\"pools\"] = ec.pools\n", + " if ec.depends_on:\n", + " entry[\"depends_on\"] = ec.depends_on\n", + " if ec.resources:\n", + " entry[\"resources\"] = ec.resources\n", + " break\n", + "\n", + " execution_order.append(entry)\n", + "\n", + " result = {\n", + " \"source_nodes\": source_nodes,\n", + " \"sink_nodes\": sink_nodes,\n", + " \"execution_order\": execution_order,\n", + " \"total_nodes\": len(node_names),\n", + " \"total_edges\": len(net_config.graph.edges),\n", + " }\n", + "\n", + " output_json(result, pretty)" + ], + "execution_count": null, + "outputs": [], + "metadata": {} + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/netrun-cli/nbs/netrun_cli/03_actions.ipynb b/netrun-cli/nbs/netrun_cli/03_actions.ipynb new file mode 100644 index 00000000..e92318de --- /dev/null +++ b/netrun-cli/nbs/netrun_cli/03_actions.ipynb @@ -0,0 +1,156 @@ +{ + "cells": [ + { + "cell_type": "code", + "source": [ + "#|default_exp _actions" + ], + "execution_count": null, + "outputs": [], + "metadata": {} + }, + { + "cell_type": "code", + "source": [ + "#|hide\n", + "from nblite import nbl_export; nbl_export();" + ], + "execution_count": null, + "outputs": [], + "metadata": {} + }, + { + "cell_type": "code", + "source": [ + "#|export\n", + "import asyncio\n", + "from typing import Annotated, Optional\n", + "\n", + "import typer\n", + "\n", + "from netrun_cli._helpers import ConfigOpt, PrettyOpt, load_config, output_json, get_node_by_name\n", + "from netrun.tools._helpers import (\n", + " get_available_actions,\n", + " build_action_context,\n", + ")\n", + "from netrun.tools._execute import execute_action\n", + "from netrun.tools._models import ActionConfig" + ], + "execution_count": null, + "outputs": [], + "metadata": {} + }, + { + "cell_type": "markdown", + "source": [ + "# Actions Commands\n", + "\n", + "List and run actions defined in a netrun config." + ], + "metadata": {} + }, + { + "cell_type": "code", + "source": [ + "#|export\n", + "actions_app = typer.Typer(help=\"List and run actions.\", no_args_is_help=True)\n", + "\n", + "NodeOpt = Annotated[Optional[str], typer.Option(\"--node\", \"-n\", help=\"Node name for node-level actions.\")]\n", + "\n", + "\n", + "@actions_app.command(\"list\")\n", + "def actions_list(\n", + " config: ConfigOpt = None,\n", + " node_name: NodeOpt = None,\n", + " pretty: PrettyOpt = True,\n", + ") -> None:\n", + " \"\"\"List available actions.\"\"\"\n", + " net_config, config_path = load_config(config)\n", + "\n", + " node_extra = None\n", + " if node_name:\n", + " n = get_node_by_name(net_config, node_name)\n", + " node_extra = n.extra\n", + "\n", + " actions = get_available_actions(net_config.graph.extra, node_extra)\n", + " result = [a.model_dump() for a in actions]\n", + " output_json(result, pretty)\n", + "\n", + "\n", + "@actions_app.command(\"run\")\n", + "def actions_run(\n", + " action_id: Annotated[str, typer.Argument(help=\"Action ID to run.\")],\n", + " node_name: Annotated[Optional[str], typer.Argument(help=\"Node name to run the action on.\")] = None,\n", + " config: ConfigOpt = None,\n", + " global_only: Annotated[bool, typer.Option(\"--global\", \"-g\", help=\"Run with project-level context only (no node).\")] = False,\n", + " timeout: Annotated[float, typer.Option(\"--timeout\", \"-t\", help=\"Timeout in seconds.\")] = 30.0,\n", + " pretty: PrettyOpt = True,\n", + ") -> None:\n", + " \"\"\"Run an action by ID on a specific node.\"\"\"\n", + " if not node_name and not global_only:\n", + " typer.echo(\"Error: provide a node name or use --global for project-level context.\", err=True)\n", + " raise typer.Exit(2)\n", + " if node_name and global_only:\n", + " typer.echo(\"Error: cannot use --global with a node name.\", err=True)\n", + " raise typer.Exit(2)\n", + "\n", + " net_config, config_path = load_config(config)\n", + "\n", + " node_extra = None\n", + " node_execution_config = None\n", + " node_config = None\n", + " if node_name:\n", + " n = get_node_by_name(net_config, node_name)\n", + " node_extra = n.extra\n", + " if n.execution_config:\n", + " node_execution_config = n.execution_config.model_dump(exclude_none=True)\n", + " node_config = n.model_dump_json(exclude_none=True)\n", + "\n", + " actions = get_available_actions(net_config.graph.extra, node_extra)\n", + " action: ActionConfig | None = None\n", + " for a in actions:\n", + " if a.id == action_id:\n", + " action = a\n", + " break\n", + "\n", + " if action is None:\n", + " typer.echo(f\"Error: action '{action_id}' not found.\", err=True)\n", + " raise typer.Exit(1)\n", + "\n", + " # Extract global node_vars as plain dict for build_action_context\n", + " global_node_vars = None\n", + " if net_config.node_vars:\n", + " global_node_vars = {k: v.model_dump() for k, v in net_config.node_vars.items()}\n", + "\n", + " context = build_action_context(\n", + " graph_extra=net_config.graph.extra,\n", + " node_name=node_name,\n", + " node_extra=node_extra,\n", + " net_file_path=str(config_path),\n", + " project_root=str(net_config.project_root_path),\n", + " global_node_vars=global_node_vars,\n", + " node_execution_config=node_execution_config,\n", + " node_config=node_config,\n", + " )\n", + "\n", + " result = asyncio.run(execute_action(action, context, timeout=timeout))\n", + " output_json(result.model_dump(), pretty)\n", + "\n", + " if not result.success:\n", + " raise typer.Exit(1)" + ], + "execution_count": null, + "outputs": [], + "metadata": {} + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/netrun-cli/nbs/netrun_cli/04_recipes.ipynb b/netrun-cli/nbs/netrun_cli/04_recipes.ipynb new file mode 100644 index 00000000..dd7543a8 --- /dev/null +++ b/netrun-cli/nbs/netrun_cli/04_recipes.ipynb @@ -0,0 +1,142 @@ +{ + "cells": [ + { + "cell_type": "code", + "source": [ + "#|default_exp _recipes" + ], + "execution_count": null, + "outputs": [], + "metadata": {} + }, + { + "cell_type": "code", + "source": [ + "#|hide\n", + "from nblite import nbl_export; nbl_export();" + ], + "execution_count": null, + "outputs": [], + "metadata": {} + }, + { + "cell_type": "code", + "source": [ + "#|export\n", + "import json\n", + "from pathlib import Path\n", + "from typing import Annotated, Optional\n", + "\n", + "import typer\n", + "\n", + "from netrun_cli._helpers import ConfigOpt, PrettyOpt, load_raw_data, output_json\n", + "from netrun.tools._helpers import get_recipes\n", + "from netrun.tools._recipes import execute_recipe, get_recipe_prompts" + ], + "execution_count": null, + "outputs": [], + "metadata": {} + }, + { + "cell_type": "markdown", + "source": [ + "# Recipes Commands\n", + "\n", + "List and run recipes defined in a netrun config." + ], + "metadata": {} + }, + { + "cell_type": "code", + "source": [ + "#|export\n", + "recipes_app = typer.Typer(help=\"List and run recipes.\", no_args_is_help=True)\n", + "\n", + "\n", + "@recipes_app.command(\"list\")\n", + "def recipes_list(\n", + " config: ConfigOpt = None,\n", + " pretty: PrettyOpt = True,\n", + ") -> None:\n", + " \"\"\"List available recipes.\"\"\"\n", + " raw, config_path = load_raw_data(config)\n", + " recipes = get_recipes(raw)\n", + " result = {name: r.model_dump() for name, r in recipes.items()}\n", + " output_json(result, pretty)\n", + "\n", + "\n", + "@recipes_app.command(\"run\")\n", + "def recipes_run(\n", + " name: Annotated[str, typer.Argument(help=\"Recipe name to run.\")],\n", + " config: ConfigOpt = None,\n", + " inputs: Annotated[Optional[str], typer.Option(\"--inputs\", \"-i\", help=\"JSON string of input values.\")] = None,\n", + " output: Annotated[Optional[str], typer.Option(\"--output\", \"-o\", help=\"Output file path for modified config.\")] = None,\n", + " pretty: PrettyOpt = True,\n", + ") -> None:\n", + " \"\"\"Run a recipe with JSON inputs.\"\"\"\n", + " raw, config_path = load_raw_data(config)\n", + " recipes = get_recipes(raw)\n", + "\n", + " if name not in recipes:\n", + " typer.echo(f\"Error: recipe '{name}' not found.\", err=True)\n", + " raise typer.Exit(1)\n", + "\n", + " recipe_cfg = recipes[name]\n", + " recipe_path = Path(recipe_cfg.path)\n", + " if not recipe_path.is_absolute():\n", + " recipe_path = config_path.parent / recipe_path\n", + "\n", + " if not recipe_path.exists():\n", + " typer.echo(f\"Error: recipe file not found: {recipe_path}\", err=True)\n", + " raise typer.Exit(1)\n", + "\n", + " # Parse inputs\n", + " input_values: dict = {}\n", + " if inputs:\n", + " try:\n", + " input_values = json.loads(inputs)\n", + " except json.JSONDecodeError as e:\n", + " typer.echo(f\"Error: invalid JSON inputs: {e}\", err=True)\n", + " raise typer.Exit(1)\n", + "\n", + " # If no inputs given, check if recipe has prompts and show them\n", + " if not inputs:\n", + " prompts = get_recipe_prompts(recipe_path, raw)\n", + " if prompts:\n", + " typer.echo(\"Recipe requires inputs. Use --inputs with JSON:\", err=True)\n", + " for p in prompts:\n", + " info = f\" {p.name} ({p.type}): {p.label}\"\n", + " if p.default is not None:\n", + " info += f\" [default: {p.default}]\"\n", + " if p.options:\n", + " info += f\" options: {p.options}\"\n", + " typer.echo(info, err=True)\n", + " raise typer.Exit(1)\n", + "\n", + " try:\n", + " modified = execute_recipe(recipe_path, raw, input_values)\n", + " except Exception as e:\n", + " typer.echo(f\"Error running recipe: {e}\", err=True)\n", + " raise typer.Exit(1)\n", + "\n", + " if output:\n", + " Path(output).write_text(json.dumps(modified, indent=2, default=str))\n", + " typer.echo(f\"Written to {output}\")\n", + " else:\n", + " output_json(modified, pretty)" + ], + "execution_count": null, + "outputs": [], + "metadata": {} + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/netrun-cli/nbs/netrun_cli/05_download_agents.ipynb b/netrun-cli/nbs/netrun_cli/05_download_agents.ipynb new file mode 100644 index 00000000..b070fe26 --- /dev/null +++ b/netrun-cli/nbs/netrun_cli/05_download_agents.ipynb @@ -0,0 +1,124 @@ +{ + "cells": [ + { + "cell_type": "code", + "source": [ + "#|default_exp _download_agents" + ], + "execution_count": null, + "outputs": [], + "metadata": {} + }, + { + "cell_type": "code", + "source": [ + "#|hide\n", + "from nblite import nbl_export; nbl_export();" + ], + "execution_count": null, + "outputs": [], + "metadata": {} + }, + { + "cell_type": "code", + "source": [ + "#|export\n", + "import json\n", + "import urllib.request\n", + "import urllib.error\n", + "from pathlib import Path\n", + "from typing import Annotated, Optional\n", + "\n", + "import typer" + ], + "execution_count": null, + "outputs": [], + "metadata": {} + }, + { + "cell_type": "markdown", + "source": [ + "# download-agents Command\n", + "\n", + "Downloads the `agents/` directory from the netrun GitHub repository\n", + "into a local directory, giving AI agents (like Claude Code) the right context." + ], + "metadata": {} + }, + { + "cell_type": "code", + "source": [ + "#|export\n", + "_REPO_OWNER = \"lukastk\"\n", + "_REPO_NAME = \"netrun\"\n", + "\n", + "\n", + "def download_agents(\n", + " output_dir: Annotated[str, typer.Argument(help=\"Local directory to download agents into.\")] = \"./agents\",\n", + " branch: Annotated[str, typer.Option(\"--branch\", \"-b\", help=\"Branch to download from.\")] = \"main\",\n", + ") -> None:\n", + " \"\"\"Download agent docs from the netrun GitHub repository.\"\"\"\n", + " out = Path(output_dir).resolve()\n", + "\n", + " # Fetch the repo tree from GitHub API\n", + " tree_url = f\"https://api.github.com/repos/{_REPO_OWNER}/{_REPO_NAME}/git/trees/{branch}?recursive=1\"\n", + " typer.echo(f\"Fetching file list from {_REPO_OWNER}/{_REPO_NAME} (branch: {branch})...\")\n", + "\n", + " try:\n", + " req = urllib.request.Request(tree_url, headers={\"Accept\": \"application/vnd.github+json\"})\n", + " with urllib.request.urlopen(req, timeout=30) as resp:\n", + " if resp.status != 200:\n", + " typer.echo(f\"Error: GitHub API returned status {resp.status}\", err=True)\n", + " raise typer.Exit(1)\n", + " tree_data = json.loads(resp.read().decode())\n", + " except urllib.error.URLError as e:\n", + " typer.echo(f\"Error: failed to fetch tree from GitHub: {e}\", err=True)\n", + " raise typer.Exit(1)\n", + "\n", + " # Filter for files under agents/\n", + " agent_files = [\n", + " entry[\"path\"]\n", + " for entry in tree_data.get(\"tree\", [])\n", + " if entry[\"path\"].startswith(\"agents/\") and entry[\"type\"] == \"blob\"\n", + " ]\n", + "\n", + " if not agent_files:\n", + " typer.echo(\"No files found under agents/ in the repository.\", err=True)\n", + " raise typer.Exit(1)\n", + "\n", + " typer.echo(f\"Found {len(agent_files)} file(s) to download.\\n\")\n", + "\n", + " downloaded = 0\n", + " for path in agent_files:\n", + " # Strip the leading \"agents/\" prefix for the local relative path\n", + " rel = path[len(\"agents/\"):]\n", + " dest = out / rel\n", + " dest.parent.mkdir(parents=True, exist_ok=True)\n", + "\n", + " raw_url = f\"https://raw.githubusercontent.com/{_REPO_OWNER}/{_REPO_NAME}/{branch}/{path}\"\n", + " try:\n", + " with urllib.request.urlopen(raw_url, timeout=30) as resp:\n", + " content = resp.read()\n", + " dest.write_bytes(content)\n", + " typer.echo(f\" {rel}\")\n", + " downloaded += 1\n", + " except urllib.error.URLError as e:\n", + " typer.echo(f\" FAILED {rel}: {e}\", err=True)\n", + "\n", + " typer.echo(f\"\\nDownloaded {downloaded}/{len(agent_files)} file(s) to {out}\")" + ], + "execution_count": null, + "outputs": [], + "metadata": {} + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/netrun-cli/nbs/netrun_cli/06_graph.ipynb b/netrun-cli/nbs/netrun_cli/06_graph.ipynb new file mode 100644 index 00000000..513cbfa4 --- /dev/null +++ b/netrun-cli/nbs/netrun_cli/06_graph.ipynb @@ -0,0 +1,496 @@ +{ + "cells": [ + { + "cell_type": "code", + "source": [ + "#|default_exp _graph" + ], + "execution_count": null, + "outputs": [], + "metadata": {} + }, + { + "cell_type": "code", + "source": [ + "#|hide\n", + "from nblite import nbl_export; nbl_export();" + ], + "execution_count": null, + "outputs": [], + "metadata": {} + }, + { + "cell_type": "code", + "source": [ + "#|export\n", + "import json\n", + "import sys\n", + "from pathlib import Path\n", + "from typing import Annotated, Any, Optional\n", + "\n", + "import typer\n", + "\n", + "from netrun_cli._helpers import (\n", + " ConfigOpt,\n", + " PrettyOpt,\n", + " find_config,\n", + " load_raw_data,\n", + " output_json,\n", + " write_config_data,\n", + " deep_merge,\n", + " auto_position,\n", + " validate_after_write,\n", + ")" + ], + "execution_count": null, + "outputs": [], + "metadata": {} + }, + { + "cell_type": "markdown", + "source": [ + "# Graph Management CLI Commands\n", + "\n", + "Commands for adding, removing, and editing nodes and edges in netrun config files.\n", + "All mutating commands operate on raw dicts (not Pydantic models) to preserve\n", + "arbitrary extra fields and work on partially invalid configs." + ], + "metadata": {} + }, + { + "cell_type": "markdown", + "source": [ + "## add-node" + ], + "metadata": {} + }, + { + "cell_type": "code", + "source": [ + "#|export\n", + "NoValidateOpt = Annotated[bool, typer.Option(\"--no-validate\", help=\"Skip post-write validation.\")]\n", + "\n", + "\n", + "def add_node(\n", + " name: Annotated[str, typer.Argument(help=\"Name for the new node.\")],\n", + " config: ConfigOpt = None,\n", + " factory: Annotated[Optional[str], typer.Option(\"--factory\", \"-f\", help=\"Factory import path.\")] = None,\n", + " factory_arg: Annotated[Optional[list[str]], typer.Option(\"--factory-arg\", help=\"Factory arg as key=value (repeatable).\")] = None,\n", + " in_ports: Annotated[Optional[str], typer.Option(\"--in-ports\", help=\"Comma-separated input port names.\")] = None,\n", + " out_ports: Annotated[Optional[str], typer.Option(\"--out-ports\", help=\"Comma-separated output port names.\")] = None,\n", + " position: Annotated[Optional[str], typer.Option(\"--position\", help=\"UI position as x,y.\")] = None,\n", + " from_json: Annotated[bool, typer.Option(\"--json\", help=\"Read full node dict from stdin.\")] = False,\n", + " pretty: PrettyOpt = True,\n", + " no_validate: NoValidateOpt = False,\n", + ") -> None:\n", + " \"\"\"Add a node to the graph.\"\"\"\n", + " data, config_path = load_raw_data(config)\n", + " graph = data.setdefault(\"graph\", {})\n", + " nodes_list: list[dict] = graph.setdefault(\"nodes\", [])\n", + "\n", + " # Check name uniqueness\n", + " for existing in nodes_list:\n", + " if existing.get(\"name\") == name:\n", + " typer.echo(f\"Error: node '{name}' already exists.\", err=True)\n", + " raise typer.Exit(1)\n", + "\n", + " # Build node dict\n", + " if from_json:\n", + " raw_input = sys.stdin.read()\n", + " try:\n", + " node_dict = json.loads(raw_input)\n", + " except json.JSONDecodeError as e:\n", + " typer.echo(f\"Error: invalid JSON from stdin: {e}\", err=True)\n", + " raise typer.Exit(1)\n", + " node_dict[\"name\"] = name\n", + " else:\n", + " node_dict: dict[str, Any] = {\"name\": name}\n", + " if factory:\n", + " node_dict[\"factory\"] = factory\n", + " if factory_arg:\n", + " fa: dict[str, Any] = {}\n", + " for item in factory_arg:\n", + " if \"=\" not in item:\n", + " typer.echo(f\"Error: --factory-arg must be key=value, got: {item}\", err=True)\n", + " raise typer.Exit(1)\n", + " k, v = item.split(\"=\", 1)\n", + " fa[k] = v\n", + " node_dict[\"factory_args\"] = fa\n", + " if in_ports:\n", + " node_dict[\"in_ports\"] = {p.strip(): {} for p in in_ports.split(\",\")}\n", + " if out_ports:\n", + " node_dict[\"out_ports\"] = {p.strip(): {} for p in out_ports.split(\",\")}\n", + "\n", + " # Position\n", + " if position:\n", + " parts = position.split(\",\")\n", + " if len(parts) != 2:\n", + " typer.echo(\"Error: --position must be x,y\", err=True)\n", + " raise typer.Exit(1)\n", + " pos = {\"x\": float(parts[0]), \"y\": float(parts[1])}\n", + " else:\n", + " pos = auto_position(nodes_list)\n", + " node_dict.setdefault(\"extra\", {}).setdefault(\"ui\", {})[\"position\"] = pos\n", + "\n", + " nodes_list.append(node_dict)\n", + " write_config_data(data, config_path)\n", + "\n", + " if not no_validate:\n", + " validate_after_write(config_path)\n", + "\n", + " output_json(node_dict, pretty)" + ], + "execution_count": null, + "outputs": [], + "metadata": {} + }, + { + "cell_type": "markdown", + "source": [ + "## remove-node" + ], + "metadata": {} + }, + { + "cell_type": "code", + "source": [ + "#|export\n", + "def _edge_references_node(edge: dict, node_name: str) -> bool:\n", + " \"\"\"Check if an edge references a node.\"\"\"\n", + " if edge.get(\"source_node\") == node_name:\n", + " return True\n", + " if edge.get(\"target_node\") == node_name:\n", + " return True\n", + " return False\n", + "\n", + "\n", + "def remove_node(\n", + " name: Annotated[str, typer.Argument(help=\"Name of the node to remove.\")],\n", + " config: ConfigOpt = None,\n", + " pretty: PrettyOpt = True,\n", + " no_validate: NoValidateOpt = False,\n", + ") -> None:\n", + " \"\"\"Remove a node and its connected edges from the graph.\"\"\"\n", + " data, config_path = load_raw_data(config)\n", + " graph = data.get(\"graph\", {})\n", + " nodes_list: list[dict] = graph.get(\"nodes\", [])\n", + "\n", + " # Find and remove node\n", + " found = False\n", + " for i, n in enumerate(nodes_list):\n", + " if n.get(\"name\") == name:\n", + " nodes_list.pop(i)\n", + " found = True\n", + " break\n", + "\n", + " if not found:\n", + " typer.echo(f\"Error: node '{name}' not found.\", err=True)\n", + " raise typer.Exit(1)\n", + "\n", + " # Remove connected edges\n", + " edges: list[dict] = graph.get(\"edges\", [])\n", + " original_edge_count = len(edges)\n", + " graph[\"edges\"] = [e for e in edges if not _edge_references_node(e, name)]\n", + " edges_removed = original_edge_count - len(graph[\"edges\"])\n", + "\n", + " # Clean output_queues that reference this node\n", + " oq = data.get(\"output_queues\")\n", + " if isinstance(oq, dict):\n", + " keys_to_remove = []\n", + " for qname, qcfg in oq.items():\n", + " if isinstance(qcfg, dict):\n", + " ports = qcfg.get(\"ports\", [])\n", + " qcfg[\"ports\"] = [p for p in ports if not _port_ref_matches_node(p, name)]\n", + " if not qcfg[\"ports\"]:\n", + " keys_to_remove.append(qname)\n", + " elif isinstance(qcfg, str) and qcfg.startswith(f\"{name}.\"):\n", + " keys_to_remove.append(qname)\n", + " for k in keys_to_remove:\n", + " del oq[k]\n", + "\n", + " write_config_data(data, config_path)\n", + "\n", + " if not no_validate:\n", + " validate_after_write(config_path)\n", + "\n", + " output_json({\"removed\": name, \"edges_removed\": edges_removed}, pretty)\n", + "\n", + "\n", + "def _port_ref_matches_node(port_ref: Any, node_name: str) -> bool:\n", + " \"\"\"Check if a port reference (string or dict) refers to a given node.\"\"\"\n", + " if isinstance(port_ref, str):\n", + " return port_ref.split(\".\")[0] == node_name\n", + " if isinstance(port_ref, dict):\n", + " return port_ref.get(\"node_name\") == node_name\n", + " return False" + ], + "execution_count": null, + "outputs": [], + "metadata": {} + }, + { + "cell_type": "markdown", + "source": [ + "## edit-node" + ], + "metadata": {} + }, + { + "cell_type": "code", + "source": [ + "#|export\n", + "def _update_edge_node_refs(edges: list[dict], old_name: str, new_name: str) -> None:\n", + " \"\"\"Rename node references in edges.\"\"\"\n", + " for edge in edges:\n", + " if edge.get(\"source_node\") == old_name:\n", + " edge[\"source_node\"] = new_name\n", + " if edge.get(\"target_node\") == old_name:\n", + " edge[\"target_node\"] = new_name\n", + "\n", + "\n", + "def edit_node(\n", + " name: Annotated[str, typer.Argument(help=\"Name of the node to edit.\")],\n", + " config: ConfigOpt = None,\n", + " rename: Annotated[Optional[str], typer.Option(\"--rename\", help=\"Rename the node.\")] = None,\n", + " add_in_port: Annotated[Optional[list[str]], typer.Option(\"--add-in-port\", help=\"Add input port (repeatable).\")] = None,\n", + " remove_in_port: Annotated[Optional[list[str]], typer.Option(\"--remove-in-port\", help=\"Remove input port (repeatable).\")] = None,\n", + " add_out_port: Annotated[Optional[list[str]], typer.Option(\"--add-out-port\", help=\"Add output port (repeatable).\")] = None,\n", + " remove_out_port: Annotated[Optional[list[str]], typer.Option(\"--remove-out-port\", help=\"Remove output port (repeatable).\")] = None,\n", + " merge: Annotated[Optional[str], typer.Option(\"--merge\", help=\"JSON string to deep-merge into the node.\")] = None,\n", + " merge_stdin: Annotated[bool, typer.Option(\"--merge-stdin\", help=\"Read JSON from stdin and deep-merge into the node.\")] = False,\n", + " pretty: PrettyOpt = True,\n", + " no_validate: NoValidateOpt = False,\n", + ") -> None:\n", + " \"\"\"Edit an existing node in the graph.\"\"\"\n", + " data, config_path = load_raw_data(config)\n", + " graph = data.get(\"graph\", {})\n", + " nodes_list: list[dict] = graph.get(\"nodes\", [])\n", + "\n", + " # Find node\n", + " node_dict = None\n", + " for n in nodes_list:\n", + " if n.get(\"name\") == name:\n", + " node_dict = n\n", + " break\n", + " if node_dict is None:\n", + " typer.echo(f\"Error: node '{name}' not found.\", err=True)\n", + " raise typer.Exit(1)\n", + "\n", + " # Rename\n", + " if rename:\n", + " # Check uniqueness\n", + " for n in nodes_list:\n", + " if n.get(\"name\") == rename:\n", + " typer.echo(f\"Error: node '{rename}' already exists.\", err=True)\n", + " raise typer.Exit(1)\n", + " old_name = node_dict[\"name\"]\n", + " node_dict[\"name\"] = rename\n", + " # Update edge references\n", + " _update_edge_node_refs(graph.get(\"edges\", []), old_name, rename)\n", + " # Update output_queue refs\n", + " oq = data.get(\"output_queues\")\n", + " if isinstance(oq, dict):\n", + " for qcfg in oq.values():\n", + " if isinstance(qcfg, dict):\n", + " ports = qcfg.get(\"ports\", [])\n", + " for i, p in enumerate(ports):\n", + " if isinstance(p, str) and p.split(\".\")[0] == old_name:\n", + " ports[i] = rename + p[len(old_name):]\n", + " elif isinstance(p, dict) and p.get(\"node_name\") == old_name:\n", + " p[\"node_name\"] = rename\n", + "\n", + " # Port modifications\n", + " if add_in_port:\n", + " ip = node_dict.setdefault(\"in_ports\", {})\n", + " for p in add_in_port:\n", + " ip[p] = {}\n", + " if remove_in_port:\n", + " ip = node_dict.get(\"in_ports\", {})\n", + " for p in remove_in_port:\n", + " ip.pop(p, None)\n", + " if add_out_port:\n", + " op = node_dict.setdefault(\"out_ports\", {})\n", + " for p in add_out_port:\n", + " op[p] = {}\n", + " if remove_out_port:\n", + " op = node_dict.get(\"out_ports\", {})\n", + " for p in remove_out_port:\n", + " op.pop(p, None)\n", + "\n", + " # Merge\n", + " if merge:\n", + " try:\n", + " merge_data = json.loads(merge)\n", + " except json.JSONDecodeError as e:\n", + " typer.echo(f\"Error: invalid --merge JSON: {e}\", err=True)\n", + " raise typer.Exit(1)\n", + " merged = deep_merge(node_dict, merge_data)\n", + " # Replace in-place\n", + " node_dict.clear()\n", + " node_dict.update(merged)\n", + " if merge_stdin:\n", + " raw_input = sys.stdin.read()\n", + " try:\n", + " merge_data = json.loads(raw_input)\n", + " except json.JSONDecodeError as e:\n", + " typer.echo(f\"Error: invalid JSON from stdin: {e}\", err=True)\n", + " raise typer.Exit(1)\n", + " merged = deep_merge(node_dict, merge_data)\n", + " node_dict.clear()\n", + " node_dict.update(merged)\n", + "\n", + " write_config_data(data, config_path)\n", + "\n", + " if not no_validate:\n", + " validate_after_write(config_path)\n", + "\n", + " output_json(node_dict, pretty)" + ], + "execution_count": null, + "outputs": [], + "metadata": {} + }, + { + "cell_type": "markdown", + "source": [ + "## add-edge" + ], + "metadata": {} + }, + { + "cell_type": "code", + "source": [ + "#|export\n", + "def add_edge(\n", + " source_node_arg: Annotated[str, typer.Argument(help=\"Source node name.\")],\n", + " source_port_arg: Annotated[str, typer.Argument(help=\"Source port name.\")],\n", + " target_node_arg: Annotated[str, typer.Argument(help=\"Target node name.\")],\n", + " target_port_arg: Annotated[str, typer.Argument(help=\"Target port name.\")],\n", + " config: ConfigOpt = None,\n", + " dependency: Annotated[bool, typer.Option(\"--dependency\", help=\"Mark edge as a dependency edge.\")] = False,\n", + " pretty: PrettyOpt = True,\n", + " no_validate: NoValidateOpt = False,\n", + ") -> None:\n", + " \"\"\"Add an edge between two ports.\"\"\"\n", + " data, config_path = load_raw_data(config)\n", + " graph = data.setdefault(\"graph\", {})\n", + " nodes_list: list[dict] = graph.get(\"nodes\", [])\n", + " edges: list[dict] = graph.setdefault(\"edges\", [])\n", + "\n", + " node_names = {n.get(\"name\") for n in nodes_list}\n", + "\n", + " if source_node_arg not in node_names:\n", + " typer.echo(f\"Error: source node '{source_node_arg}' not found.\", err=True)\n", + " raise typer.Exit(1)\n", + " if target_node_arg not in node_names:\n", + " typer.echo(f\"Error: target node '{target_node_arg}' not found.\", err=True)\n", + " raise typer.Exit(1)\n", + "\n", + " # Warn on fan-out\n", + " for e in edges:\n", + " if e.get(\"source_node\") == source_node_arg and e.get(\"source_port\") == source_port_arg:\n", + " typer.echo(\n", + " f\"Warning: fan-out — '{source_node_arg}.{source_port_arg}' already has an edge. \"\n", + " \"Consider using 'netrun.node_factories.broadcast' to replicate output to multiple targets.\",\n", + " err=True,\n", + " )\n", + " break\n", + "\n", + " edge_dict: dict[str, Any] = {\n", + " \"source_node\": source_node_arg,\n", + " \"source_port\": source_port_arg,\n", + " \"target_node\": target_node_arg,\n", + " \"target_port\": target_port_arg,\n", + " }\n", + " if dependency:\n", + " edge_dict[\"dependency\"] = True\n", + "\n", + " edges.append(edge_dict)\n", + " write_config_data(data, config_path)\n", + "\n", + " if not no_validate:\n", + " validate_after_write(config_path)\n", + "\n", + " output_json(edge_dict, pretty)" + ], + "execution_count": null, + "outputs": [], + "metadata": {} + }, + { + "cell_type": "markdown", + "source": [ + "## remove-edge" + ], + "metadata": {} + }, + { + "cell_type": "code", + "source": [ + "#|export\n", + "def _edge_matches(edge: dict, source_node: str, source_port: str, target_node: str, target_port: str) -> bool:\n", + " \"\"\"Check if an edge matches the given source and target fields.\"\"\"\n", + " return (\n", + " edge.get(\"source_node\") == source_node\n", + " and edge.get(\"source_port\") == source_port\n", + " and edge.get(\"target_node\") == target_node\n", + " and edge.get(\"target_port\") == target_port\n", + " )\n", + "\n", + "\n", + "def remove_edge(\n", + " source_node_arg: Annotated[str, typer.Argument(help=\"Source node name.\")],\n", + " source_port_arg: Annotated[str, typer.Argument(help=\"Source port name.\")],\n", + " target_node_arg: Annotated[str, typer.Argument(help=\"Target node name.\")],\n", + " target_port_arg: Annotated[str, typer.Argument(help=\"Target port name.\")],\n", + " config: ConfigOpt = None,\n", + " pretty: PrettyOpt = True,\n", + " no_validate: NoValidateOpt = False,\n", + ") -> None:\n", + " \"\"\"Remove an edge between two ports.\"\"\"\n", + " data, config_path = load_raw_data(config)\n", + " graph = data.get(\"graph\", {})\n", + " edges: list[dict] = graph.get(\"edges\", [])\n", + "\n", + " found = False\n", + " for i, e in enumerate(edges):\n", + " if _edge_matches(e, source_node_arg, source_port_arg, target_node_arg, target_port_arg):\n", + " edges.pop(i)\n", + " found = True\n", + " break\n", + "\n", + " if not found:\n", + " typer.echo(\n", + " f\"Error: edge '{source_node_arg}.{source_port_arg}' -> \"\n", + " f\"'{target_node_arg}.{target_port_arg}' not found.\",\n", + " err=True,\n", + " )\n", + " raise typer.Exit(1)\n", + "\n", + " write_config_data(data, config_path)\n", + "\n", + " if not no_validate:\n", + " validate_after_write(config_path)\n", + "\n", + " output_json({\n", + " \"removed_source_node\": source_node_arg,\n", + " \"removed_source_port\": source_port_arg,\n", + " \"removed_target_node\": target_node_arg,\n", + " \"removed_target_port\": target_port_arg,\n", + " }, pretty)" + ], + "execution_count": null, + "outputs": [], + "metadata": {} + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/netrun-cli/nbs/tests/test_cli.ipynb b/netrun-cli/nbs/tests/test_cli.ipynb new file mode 100644 index 00000000..4682f5fc --- /dev/null +++ b/netrun-cli/nbs/tests/test_cli.ipynb @@ -0,0 +1,794 @@ +{ + "cells": [ + { + "cell_type": "code", + "source": [ + "#|default_exp test_cli" + ], + "execution_count": null, + "outputs": [], + "metadata": {} + }, + { + "cell_type": "code", + "source": [ + "#|hide\n", + "from nblite import nbl_export; nbl_export();" + ], + "execution_count": null, + "outputs": [], + "metadata": {} + }, + { + "cell_type": "code", + "source": [ + "#|export\n", + "import json\n", + "import os\n", + "from pathlib import Path\n", + "\n", + "import pytest\n", + "from typer.testing import CliRunner\n", + "\n", + "from netrun_cli._app import app" + ], + "execution_count": null, + "outputs": [], + "metadata": {} + }, + { + "cell_type": "code", + "source": [ + "#|export\n", + "runner = CliRunner()\n", + "\n", + "SAMPLE_DIR = Path(__file__).resolve().parent.parent.parent.parent / \"sample_projects\"\n", + "BASIC_CONFIG = str(SAMPLE_DIR / \"00_basic_net_project\" / \"main.netrun.json\")\n", + "POOLS_CONFIG = str(SAMPLE_DIR / \"01_thread_and_process_pools\" / \"main.netrun.json\")" + ], + "execution_count": null, + "outputs": [], + "metadata": {} + }, + { + "cell_type": "markdown", + "source": [ + "## Test validate" + ], + "metadata": {} + }, + { + "cell_type": "code", + "source": [ + "#|export\n", + "def test_validate_basic():\n", + " result = runner.invoke(app, [\"validate\", \"-c\", BASIC_CONFIG])\n", + " assert result.exit_code == 0\n", + " data = json.loads(result.stdout)\n", + " assert data[\"valid\"] is True\n", + " assert data[\"nodes\"] == 4\n", + " assert data[\"edges\"] == 2\n", + "\n", + "\n", + "def test_validate_pools():\n", + " result = runner.invoke(app, [\"validate\", \"-c\", POOLS_CONFIG])\n", + " assert result.exit_code == 0\n", + " data = json.loads(result.stdout)\n", + " assert data[\"valid\"] is True\n", + " assert data[\"nodes\"] == 6\n", + "\n", + "\n", + "def test_validate_not_found():\n", + " result = runner.invoke(app, [\"validate\", \"-c\", \"/nonexistent/file.netrun.json\"])\n", + " assert result.exit_code == 1\n", + "\n", + "\n", + "def test_validate_pretty():\n", + " result = runner.invoke(app, [\"validate\", \"-c\", BASIC_CONFIG, \"--pretty\"])\n", + " assert result.exit_code == 0\n", + " data = json.loads(result.stdout)\n", + " assert data[\"valid\"] is True" + ], + "execution_count": null, + "outputs": [], + "metadata": {} + }, + { + "cell_type": "markdown", + "source": [ + "## Test structure" + ], + "metadata": {} + }, + { + "cell_type": "code", + "source": [ + "#|export\n", + "def test_structure_basic():\n", + " result = runner.invoke(app, [\"structure\", \"-c\", BASIC_CONFIG])\n", + " assert result.exit_code == 0\n", + " data = json.loads(result.stdout)\n", + " assert len(data[\"nodes\"]) == 4\n", + " assert len(data[\"edges\"]) == 2\n", + " # Check edge format\n", + " assert data[\"edges\"][0][\"source\"] == \"double.out\"\n", + " assert data[\"edges\"][0][\"target\"] == \"add.a\"\n", + "\n", + "\n", + "def test_structure_node_has_factory():\n", + " result = runner.invoke(app, [\"structure\", \"-c\", BASIC_CONFIG])\n", + " data = json.loads(result.stdout)\n", + " node = data[\"nodes\"][0]\n", + " assert node[\"name\"] == \"double\"\n", + " assert node[\"factory\"] == \"netrun.node_factories.from_function\"\n", + " assert node[\"factory_args\"][\"func\"] == \"nodes.double\"" + ], + "execution_count": null, + "outputs": [], + "metadata": {} + }, + { + "cell_type": "markdown", + "source": [ + "## Test convert" + ], + "metadata": {} + }, + { + "cell_type": "code", + "source": [ + "#|export\n", + "def test_convert_json_to_toml():\n", + " result = runner.invoke(app, [\"convert\", BASIC_CONFIG])\n", + " assert result.exit_code == 0\n", + " # Should contain TOML syntax\n", + " assert \"[extra]\" in result.stdout or \"[[graph.nodes]]\" in result.stdout\n", + "\n", + "\n", + "def test_convert_not_found():\n", + " result = runner.invoke(app, [\"convert\", \"/nonexistent.netrun.json\"])\n", + " assert result.exit_code == 1\n", + "\n", + "\n", + "def test_convert_bad_extension():\n", + " result = runner.invoke(app, [\"convert\", \"/some/file.txt\"])\n", + " assert result.exit_code == 1" + ], + "execution_count": null, + "outputs": [], + "metadata": {} + }, + { + "cell_type": "markdown", + "source": [ + "## Test factory-info" + ], + "metadata": {} + }, + { + "cell_type": "code", + "source": [ + "#|export\n", + "def test_factory_info():\n", + " result = runner.invoke(app, [\"factory-info\", \"netrun.node_factories.from_function\", \"--pretty\"])\n", + " assert result.exit_code == 0\n", + " data = json.loads(result.stdout)\n", + " assert data[\"factory\"] == \"netrun.node_factories.from_function\"\n", + " assert data[\"type\"] == \"node\"\n", + " assert \"description\" in data\n", + " assert len(data[\"params\"]) >= 1\n", + " assert data[\"params\"][0][\"name\"] == \"func\"\n", + " assert data[\"params\"][0][\"required\"] is True\n", + "\n", + "\n", + "def test_factory_info_bad_module():\n", + " result = runner.invoke(app, [\"factory-info\", \"nonexistent.module\"])\n", + " assert result.exit_code == 1" + ], + "execution_count": null, + "outputs": [], + "metadata": {} + }, + { + "cell_type": "markdown", + "source": [ + "## Test info" + ], + "metadata": {} + }, + { + "cell_type": "code", + "source": [ + "#|export\n", + "def test_info_basic():\n", + " result = runner.invoke(app, [\"info\", \"-c\", BASIC_CONFIG, \"--pretty\"])\n", + " assert result.exit_code == 0\n", + " data = json.loads(result.stdout)\n", + " assert data[\"nodes\"] == 4\n", + " assert data[\"edges\"] == 2\n", + " assert data[\"recipes\"] == 0\n", + "\n", + "\n", + "def test_info_pools():\n", + " result = runner.invoke(app, [\"info\", \"-c\", POOLS_CONFIG, \"--pretty\"])\n", + " assert result.exit_code == 0\n", + " data = json.loads(result.stdout)\n", + " assert data[\"nodes\"] == 6\n", + " assert \"pools\" in data\n", + " assert data[\"pools\"][\"threads\"] == \"thread\"\n", + " assert data[\"pools\"][\"processes\"] == \"multiprocess\"\n", + " assert data[\"recipes\"] == 1" + ], + "execution_count": null, + "outputs": [], + "metadata": {} + }, + { + "cell_type": "markdown", + "source": [ + "## Test nodes" + ], + "metadata": {} + }, + { + "cell_type": "code", + "source": [ + "#|export\n", + "def test_nodes_basic():\n", + " result = runner.invoke(app, [\"nodes\", \"-c\", BASIC_CONFIG])\n", + " assert result.exit_code == 0\n", + " data = json.loads(result.stdout)\n", + " assert len(data) == 4\n", + " names = [n[\"name\"] for n in data]\n", + " assert \"double\" in names\n", + " assert \"add\" in names\n", + " assert \"format_result\" in names\n", + " assert \"analyze\" in names" + ], + "execution_count": null, + "outputs": [], + "metadata": {} + }, + { + "cell_type": "markdown", + "source": [ + "## Test node" + ], + "metadata": {} + }, + { + "cell_type": "code", + "source": [ + "#|export\n", + "def test_node_detail():\n", + " result = runner.invoke(app, [\"node\", \"double\", \"-c\", BASIC_CONFIG, \"--pretty\"])\n", + " assert result.exit_code == 0\n", + " data = json.loads(result.stdout)\n", + " assert data[\"name\"] == \"double\"\n", + " assert data[\"factory\"] == \"netrun.node_factories.from_function\"\n", + " assert data[\"factory_args\"][\"func\"] == \"nodes.double\"\n", + "\n", + "\n", + "def test_node_not_found():\n", + " result = runner.invoke(app, [\"node\", \"nonexistent\", \"-c\", BASIC_CONFIG])\n", + " assert result.exit_code == 1" + ], + "execution_count": null, + "outputs": [], + "metadata": {} + }, + { + "cell_type": "markdown", + "source": [ + "## Test actions" + ], + "metadata": {} + }, + { + "cell_type": "code", + "source": [ + "#|export\n", + "def test_actions_list_basic():\n", + " result = runner.invoke(app, [\"actions\", \"list\", \"-c\", BASIC_CONFIG])\n", + " assert result.exit_code == 0\n", + " data = json.loads(result.stdout)\n", + " assert len(data) == 2\n", + " ids = [a[\"id\"] for a in data]\n", + " assert \"action-show-info\" in ids\n", + " assert \"action-run-project\" in ids\n", + "\n", + "\n", + "def test_actions_run_not_found():\n", + " result = runner.invoke(app, [\"actions\", \"run\", \"nonexistent\", \"double\", \"-c\", BASIC_CONFIG])\n", + " assert result.exit_code == 1\n", + "\n", + "\n", + "def test_actions_run_requires_node_or_global():\n", + " result = runner.invoke(app, [\"actions\", \"run\", \"some_action\", \"-c\", BASIC_CONFIG])\n", + " assert result.exit_code == 2\n", + "\n", + "\n", + "def test_actions_run_global_not_found():\n", + " result = runner.invoke(app, [\"actions\", \"run\", \"nonexistent\", \"--global\", \"-c\", BASIC_CONFIG])\n", + " assert result.exit_code == 1\n", + "\n", + "\n", + "def test_actions_run_global_and_node_conflict():\n", + " result = runner.invoke(app, [\"actions\", \"run\", \"some_action\", \"double\", \"--global\", \"-c\", BASIC_CONFIG])\n", + " assert result.exit_code == 2" + ], + "execution_count": null, + "outputs": [], + "metadata": {} + }, + { + "cell_type": "markdown", + "source": [ + "## Test recipes" + ], + "metadata": {} + }, + { + "cell_type": "code", + "source": [ + "#|export\n", + "def test_recipes_list_empty():\n", + " result = runner.invoke(app, [\"recipes\", \"list\", \"-c\", BASIC_CONFIG])\n", + " assert result.exit_code == 0\n", + " data = json.loads(result.stdout)\n", + " assert data == {}\n", + "\n", + "\n", + "def test_recipes_list_pools():\n", + " result = runner.invoke(app, [\"recipes\", \"list\", \"-c\", POOLS_CONFIG, \"--pretty\"])\n", + " assert result.exit_code == 0\n", + " data = json.loads(result.stdout)\n", + " assert \"add_node\" in data\n", + " assert data[\"add_node\"][\"path\"] == \"./recipes/add_node.py\"\n", + "\n", + "\n", + "def test_recipes_run_not_found():\n", + " result = runner.invoke(app, [\"recipes\", \"run\", \"nonexistent\", \"-c\", BASIC_CONFIG])\n", + " assert result.exit_code == 1" + ], + "execution_count": null, + "outputs": [], + "metadata": {} + }, + { + "cell_type": "markdown", + "source": [ + "## Test download-agents" + ], + "metadata": {} + }, + { + "cell_type": "code", + "source": [ + "#|export\n", + "from unittest.mock import patch, MagicMock\n", + "import urllib.error\n", + "\n", + "\n", + "def _make_mock_urlopen(tree_json: dict, file_contents: dict[str, bytes] | None = None):\n", + " \"\"\"Create a mock for urllib.request.urlopen that serves tree API and raw file downloads.\"\"\"\n", + " if file_contents is None:\n", + " file_contents = {}\n", + "\n", + " def mock_urlopen(req_or_url, **kwargs):\n", + " url = req_or_url.full_url if hasattr(req_or_url, \"full_url\") else req_or_url\n", + " ctx = MagicMock()\n", + " if \"api.github.com\" in url:\n", + " ctx.__enter__ = lambda s: s\n", + " ctx.__exit__ = MagicMock(return_value=False)\n", + " ctx.status = 200\n", + " ctx.read.return_value = json.dumps(tree_json).encode()\n", + " elif \"raw.githubusercontent.com\" in url:\n", + " # Find matching file content by checking which path the URL ends with\n", + " content = b\"default content\"\n", + " for path, data in file_contents.items():\n", + " if url.endswith(path):\n", + " content = data\n", + " break\n", + " ctx.__enter__ = lambda s: s\n", + " ctx.__exit__ = MagicMock(return_value=False)\n", + " ctx.read.return_value = content\n", + " else:\n", + " raise urllib.error.URLError(f\"unexpected URL: {url}\")\n", + " return ctx\n", + "\n", + " return mock_urlopen\n", + "\n", + "\n", + "def test_download_agents_success(tmp_path):\n", + " tree = {\n", + " \"tree\": [\n", + " {\"path\": \"agents/README.md\", \"type\": \"blob\"},\n", + " {\"path\": \"agents/skills/foo/SKILL.md\", \"type\": \"blob\"},\n", + " {\"path\": \"src/main.py\", \"type\": \"blob\"}, # should be ignored\n", + " {\"path\": \"agents/subdir\", \"type\": \"tree\"}, # should be ignored (not a blob)\n", + " ]\n", + " }\n", + " file_contents = {\n", + " \"agents/README.md\": b\"# Agents README\",\n", + " \"agents/skills/foo/SKILL.md\": b\"# Foo Skill\",\n", + " }\n", + " mock_fn = _make_mock_urlopen(tree, file_contents)\n", + " out_dir = str(tmp_path / \"agents\")\n", + "\n", + " with patch(\"netrun_cli._download_agents.urllib.request.urlopen\", side_effect=mock_fn):\n", + " result = runner.invoke(app, [\"download-agents\", out_dir])\n", + "\n", + " assert result.exit_code == 0\n", + " assert \"Found 2 file(s)\" in result.output\n", + " assert \"Downloaded 2/2\" in result.output\n", + "\n", + " # Verify files written\n", + " assert (tmp_path / \"agents\" / \"README.md\").read_bytes() == b\"# Agents README\"\n", + " assert (tmp_path / \"agents\" / \"skills\" / \"foo\" / \"SKILL.md\").read_bytes() == b\"# Foo Skill\"\n", + "\n", + "\n", + "def test_download_agents_no_files(tmp_path):\n", + " tree = {\"tree\": [{\"path\": \"src/main.py\", \"type\": \"blob\"}]}\n", + " mock_fn = _make_mock_urlopen(tree)\n", + " out_dir = str(tmp_path / \"agents\")\n", + "\n", + " with patch(\"netrun_cli._download_agents.urllib.request.urlopen\", side_effect=mock_fn):\n", + " result = runner.invoke(app, [\"download-agents\", out_dir])\n", + "\n", + " assert result.exit_code == 1\n", + " assert \"No files found\" in result.output\n", + "\n", + "\n", + "def test_download_agents_network_error(tmp_path):\n", + " out_dir = str(tmp_path / \"agents\")\n", + "\n", + " with patch(\n", + " \"netrun_cli._download_agents.urllib.request.urlopen\",\n", + " side_effect=urllib.error.URLError(\"connection refused\"),\n", + " ):\n", + " result = runner.invoke(app, [\"download-agents\", out_dir])\n", + "\n", + " assert result.exit_code == 1\n", + " assert \"failed to fetch tree\" in result.output\n", + "\n", + "\n", + "def test_download_agents_branch_option(tmp_path):\n", + " tree = {\"tree\": [{\"path\": \"agents/INSTRUCTIONS.md\", \"type\": \"blob\"}]}\n", + " mock_fn = _make_mock_urlopen(tree, {\"agents/INSTRUCTIONS.md\": b\"instructions\"})\n", + " out_dir = str(tmp_path / \"agents\")\n", + "\n", + " with patch(\"netrun_cli._download_agents.urllib.request.urlopen\", side_effect=mock_fn) as _:\n", + " result = runner.invoke(app, [\"download-agents\", out_dir, \"-b\", \"dev\"])\n", + "\n", + " assert result.exit_code == 0\n", + " assert \"branch: dev\" in result.output\n", + " assert (tmp_path / \"agents\" / \"INSTRUCTIONS.md\").read_bytes() == b\"instructions\"\n", + "\n", + "\n", + "def test_download_agents_partial_failure(tmp_path):\n", + " tree = {\n", + " \"tree\": [\n", + " {\"path\": \"agents/good.md\", \"type\": \"blob\"},\n", + " {\"path\": \"agents/bad.md\", \"type\": \"blob\"},\n", + " ]\n", + " }\n", + "\n", + " call_count = 0\n", + "\n", + " def mock_urlopen(req_or_url, **kwargs):\n", + " nonlocal call_count\n", + " url = req_or_url.full_url if hasattr(req_or_url, \"full_url\") else req_or_url\n", + " if \"api.github.com\" in url:\n", + " ctx = MagicMock()\n", + " ctx.__enter__ = lambda s: s\n", + " ctx.__exit__ = MagicMock(return_value=False)\n", + " ctx.status = 200\n", + " ctx.read.return_value = json.dumps(tree).encode()\n", + " return ctx\n", + " # First raw download succeeds, second fails\n", + " call_count += 1\n", + " if call_count == 1:\n", + " ctx = MagicMock()\n", + " ctx.__enter__ = lambda s: s\n", + " ctx.__exit__ = MagicMock(return_value=False)\n", + " ctx.read.return_value = b\"good content\"\n", + " return ctx\n", + " raise urllib.error.URLError(\"not found\")\n", + "\n", + " out_dir = str(tmp_path / \"agents\")\n", + "\n", + " with patch(\"netrun_cli._download_agents.urllib.request.urlopen\", side_effect=mock_urlopen):\n", + " result = runner.invoke(app, [\"download-agents\", out_dir])\n", + "\n", + " assert result.exit_code == 0\n", + " assert \"Downloaded 1/2\" in result.output\n", + " assert \"FAILED\" in result.output\n", + " assert (tmp_path / \"agents\" / \"good.md\").read_bytes() == b\"good content\"" + ], + "execution_count": null, + "outputs": [], + "metadata": {} + }, + { + "cell_type": "markdown", + "source": [ + "## Test help" + ], + "metadata": {} + }, + { + "cell_type": "code", + "source": [ + "#|export\n", + "def test_help():\n", + " result = runner.invoke(app, [\"--help\"])\n", + " assert result.exit_code == 0\n", + " assert \"validate\" in result.stdout\n", + " assert \"structure\" in result.stdout\n", + " assert \"convert\" in result.stdout\n", + " assert \"factory-info\" in result.stdout\n", + " assert \"info\" in result.stdout\n", + " assert \"nodes\" in result.stdout\n", + " assert \"node\" in result.stdout\n", + " assert \"actions\" in result.stdout\n", + " assert \"recipes\" in result.stdout\n", + " assert \"download-agents\" in result.stdout\n", + " assert \"dry-run\" in result.stdout" + ], + "execution_count": null, + "outputs": [], + "metadata": {} + }, + { + "cell_type": "markdown", + "source": [ + "## Regression: validate reports resolution failures" + ], + "metadata": {} + }, + { + "cell_type": "code", + "source": [ + "#|export\n", + "import tempfile\n", + "\n", + "def test_validate_reports_resolution_failure():\n", + " \"\"\"Test that validate reports resolution errors instead of silently swallowing them.\n", + "\n", + " Regression test: netrun validate suppresses resolution failures by catching\n", + " all exceptions and setting resolved=None, reporting valid=True.\n", + " \"\"\"\n", + " config_data = {\n", + " \"graph\": {\n", + " \"nodes\": [\n", + " {\n", + " \"name\": \"TestNode\",\n", + " \"factory\": \"nonexistent_module_that_does_not_exist.factory\",\n", + " \"factory_args\": {},\n", + " \"execution_config\": {\n", + " \"pools\": [\"main\"],\n", + " },\n", + " }\n", + " ],\n", + " \"edges\": [],\n", + " },\n", + " \"pools\": {\n", + " \"main\": {\"spec\": {\"type\": \"main\"}},\n", + " },\n", + " }\n", + " with tempfile.NamedTemporaryFile(mode=\"w\", suffix=\".netrun.json\", delete=False) as f:\n", + " json.dump(config_data, f)\n", + " f.flush()\n", + " result = runner.invoke(app, [\"validate\", \"-c\", f.name])\n", + "\n", + " # Resolution failures are reported as warnings (not errors, since resolution\n", + " # depends on Python path at runtime). The key requirement: the failure must\n", + " # NOT be silently swallowed — it must appear in the output.\n", + " data = json.loads(result.stdout)\n", + " warnings = data.get(\"warnings\", [])\n", + " errors = data.get(\"errors\", [])\n", + " all_messages = warnings + errors\n", + " assert any(\n", + " \"resolution\" in str(m).lower() or \"module\" in str(m).lower()\n", + " for m in all_messages\n", + " ), f\"validate silently swallowed resolution failure: {data}\"" + ], + "execution_count": null, + "outputs": [], + "metadata": {} + }, + { + "cell_type": "markdown", + "source": [ + "## Test node --edges" + ], + "metadata": {} + }, + { + "cell_type": "code", + "source": [ + "#|export\n", + "def test_node_edges_incoming():\n", + " result = runner.invoke(app, [\"node\", \"add\", \"--edges\", \"-c\", BASIC_CONFIG, \"--pretty\"])\n", + " assert result.exit_code == 0\n", + " data = json.loads(result.stdout)\n", + " assert \"edges\" in data\n", + " assert \"incoming\" in data[\"edges\"]\n", + " assert \"outgoing\" in data[\"edges\"]\n", + " # \"add\" has an incoming edge from \"double\"\n", + " incoming = data[\"edges\"][\"incoming\"]\n", + " assert len(incoming) >= 1\n", + " assert any(e[\"source\"] == \"double.out\" and e[\"port\"] == \"a\" for e in incoming)\n", + "\n", + "\n", + "def test_node_edges_outgoing():\n", + " result = runner.invoke(app, [\"node\", \"double\", \"--edges\", \"-c\", BASIC_CONFIG, \"--pretty\"])\n", + " assert result.exit_code == 0\n", + " data = json.loads(result.stdout)\n", + " outgoing = data[\"edges\"][\"outgoing\"]\n", + " assert len(outgoing) == 1\n", + " assert outgoing[0][\"port\"] == \"out\"\n", + " assert outgoing[0][\"target\"] == \"add.a\"\n", + "\n", + "\n", + "def test_node_edges_none():\n", + " \"\"\"Node with no connected edges returns empty lists.\"\"\"\n", + " result = runner.invoke(app, [\"node\", \"analyze\", \"--edges\", \"-c\", BASIC_CONFIG, \"--pretty\"])\n", + " assert result.exit_code == 0\n", + " data = json.loads(result.stdout)\n", + " assert data[\"edges\"][\"incoming\"] == []\n", + " assert data[\"edges\"][\"outgoing\"] == []\n", + "\n", + "\n", + "def test_node_no_edges_by_default():\n", + " result = runner.invoke(app, [\"node\", \"add\", \"-c\", BASIC_CONFIG, \"--pretty\"])\n", + " assert result.exit_code == 0\n", + " data = json.loads(result.stdout)\n", + " assert \"edges\" not in data" + ], + "execution_count": null, + "outputs": [], + "metadata": {} + }, + { + "cell_type": "markdown", + "source": [ + "## Test structure --format mermaid" + ], + "metadata": {} + }, + { + "cell_type": "code", + "source": [ + "#|export\n", + "def test_structure_mermaid():\n", + " result = runner.invoke(app, [\"structure\", \"-c\", BASIC_CONFIG, \"--format\", \"mermaid\"])\n", + " assert result.exit_code == 0\n", + " lines = result.stdout.strip().split(\"\\n\")\n", + " assert lines[0] == \"graph LR\"\n", + " # Node declarations\n", + " assert any('double[\"double\"]' in line for line in lines)\n", + " assert any('add[\"add\"]' in line for line in lines)\n", + " # Edge with label\n", + " assert any('double -->|\"out → a\"| add' in line for line in lines)\n", + "\n", + "\n", + "def test_structure_json_default():\n", + " \"\"\"Existing JSON output still works without --format.\"\"\"\n", + " result = runner.invoke(app, [\"structure\", \"-c\", BASIC_CONFIG])\n", + " assert result.exit_code == 0\n", + " data = json.loads(result.stdout)\n", + " assert \"nodes\" in data\n", + " assert \"edges\" in data\n", + "\n", + "\n", + "def test_structure_bad_format():\n", + " result = runner.invoke(app, [\"structure\", \"-c\", BASIC_CONFIG, \"--format\", \"xml\"])\n", + " assert result.exit_code == 1" + ], + "execution_count": null, + "outputs": [], + "metadata": {} + }, + { + "cell_type": "markdown", + "source": [ + "## Test dry-run" + ], + "metadata": {} + }, + { + "cell_type": "code", + "source": [ + "#|export\n", + "def test_dry_run_basic():\n", + " result = runner.invoke(app, [\"dry-run\", \"-c\", BASIC_CONFIG, \"--pretty\"])\n", + " assert result.exit_code == 0\n", + " data = json.loads(result.stdout)\n", + " assert \"source_nodes\" in data\n", + " assert \"sink_nodes\" in data\n", + " assert \"execution_order\" in data\n", + " assert data[\"total_nodes\"] == 4\n", + " assert data[\"total_edges\"] == 2\n", + " # double and analyze have no incoming edges → source nodes\n", + " assert \"double\" in data[\"source_nodes\"]\n", + " assert \"analyze\" in data[\"source_nodes\"]\n", + "\n", + "\n", + "def test_dry_run_execution_order():\n", + " result = runner.invoke(app, [\"dry-run\", \"-c\", BASIC_CONFIG])\n", + " assert result.exit_code == 0\n", + " data = json.loads(result.stdout)\n", + " order = [e[\"node\"] for e in data[\"execution_order\"]]\n", + " # double must come before add (double→add edge), add before format_result\n", + " assert order.index(\"double\") < order.index(\"add\")\n", + " assert order.index(\"add\") < order.index(\"format_result\")\n", + "\n", + "\n", + "def test_dry_run_sink_nodes():\n", + " result = runner.invoke(app, [\"dry-run\", \"-c\", BASIC_CONFIG])\n", + " assert result.exit_code == 0\n", + " data = json.loads(result.stdout)\n", + " # format_result and analyze have no outgoing data edges\n", + " assert \"format_result\" in data[\"sink_nodes\"]\n", + " assert \"analyze\" in data[\"sink_nodes\"]\n", + "\n", + "\n", + "def test_dry_run_depends_on_and_resources(tmp_path):\n", + " \"\"\"Test dry-run output includes depends_on and resources metadata.\"\"\"\n", + " config_data = {\n", + " \"graph\": {\n", + " \"nodes\": [\n", + " {\n", + " \"name\": \"A\",\n", + " \"in_ports\": {\"in\": {}},\n", + " \"out_ports\": {\"out\": {}},\n", + " \"execution_config\": {\"pools\": [\"main\"]},\n", + " },\n", + " {\n", + " \"name\": \"B\",\n", + " \"in_ports\": {\"in\": {}},\n", + " \"out_ports\": {\"out\": {}},\n", + " \"execution_config\": {\n", + " \"pools\": [\"main\"],\n", + " \"depends_on\": [\"A\"],\n", + " \"resources\": {\"gpu\": 1},\n", + " },\n", + " },\n", + " ],\n", + " \"edges\": [],\n", + " },\n", + " \"pools\": {\"main\": {\"spec\": {\"type\": \"main\"}}},\n", + " }\n", + " config_path = tmp_path / \"test.netrun.json\"\n", + " config_path.write_text(json.dumps(config_data))\n", + "\n", + " result = runner.invoke(app, [\"dry-run\", \"-c\", str(config_path), \"--pretty\"])\n", + " assert result.exit_code == 0\n", + " data = json.loads(result.stdout)\n", + "\n", + " order = [e[\"node\"] for e in data[\"execution_order\"]]\n", + " # A must come before B because of depends_on\n", + " assert order.index(\"A\") < order.index(\"B\")\n", + "\n", + " # B should have depends_on and resources in its entry\n", + " b_entry = next(e for e in data[\"execution_order\"] if e[\"node\"] == \"B\")\n", + " assert b_entry[\"depends_on\"] == [\"A\"]\n", + " assert b_entry[\"resources\"] == {\"gpu\": 1}" + ], + "execution_count": null, + "outputs": [], + "metadata": {} + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/netrun-cli/nbs/tests/test_cli_graph.ipynb b/netrun-cli/nbs/tests/test_cli_graph.ipynb new file mode 100644 index 00000000..a5f57a55 --- /dev/null +++ b/netrun-cli/nbs/tests/test_cli_graph.ipynb @@ -0,0 +1,406 @@ +{ + "cells": [ + { + "cell_type": "code", + "source": [ + "#|default_exp test_cli_graph" + ], + "execution_count": null, + "outputs": [], + "metadata": {} + }, + { + "cell_type": "code", + "source": [ + "#|hide\n", + "from nblite import nbl_export; nbl_export();" + ], + "execution_count": null, + "outputs": [], + "metadata": {} + }, + { + "cell_type": "code", + "source": [ + "#|export\n", + "import json\n", + "from pathlib import Path\n", + "\n", + "import pytest\n", + "from typer.testing import CliRunner\n", + "\n", + "from netrun_cli._app import app" + ], + "execution_count": null, + "outputs": [], + "metadata": {} + }, + { + "cell_type": "code", + "source": [ + "#|export\n", + "runner = CliRunner()\n", + "\n", + "\n", + "def _make_config(tmp_path: Path, data: dict) -> str:\n", + " \"\"\"Write a temp .netrun.json and return its path as a string.\"\"\"\n", + " p = tmp_path / \"test.netrun.json\"\n", + " p.write_text(json.dumps(data, indent=2))\n", + " return str(p)\n", + "\n", + "\n", + "def _base_config() -> dict:\n", + " \"\"\"Minimal config with two nodes and one edge.\"\"\"\n", + " return {\n", + " \"graph\": {\n", + " \"nodes\": [\n", + " {\n", + " \"name\": \"A\",\n", + " \"in_ports\": {\"in\": {}},\n", + " \"out_ports\": {\"out\": {}},\n", + " \"extra\": {\"ui\": {\"position\": {\"x\": 100, \"y\": 200}}},\n", + " },\n", + " {\n", + " \"name\": \"B\",\n", + " \"in_ports\": {\"in\": {}},\n", + " \"out_ports\": {\"out\": {}},\n", + " \"extra\": {\"ui\": {\"position\": {\"x\": 400, \"y\": 200}}},\n", + " },\n", + " ],\n", + " \"edges\": [\n", + " {\"source_node\": \"A\", \"source_port\": \"out\", \"target_node\": \"B\", \"target_port\": \"in\"},\n", + " ],\n", + " }\n", + " }" + ], + "execution_count": null, + "outputs": [], + "metadata": {} + }, + { + "cell_type": "markdown", + "source": [ + "## Test add-node" + ], + "metadata": {} + }, + { + "cell_type": "code", + "source": [ + "#|export\n", + "def test_add_node_factory(tmp_path):\n", + " cfg = _make_config(tmp_path, _base_config())\n", + " result = runner.invoke(app, [\n", + " \"add-node\", \"C\", \"-c\", cfg,\n", + " \"--factory\", \"netrun.node_factories.from_function\",\n", + " \"--factory-arg\", \"func=my_module.my_func\",\n", + " \"--no-validate\",\n", + " ])\n", + " assert result.exit_code == 0, result.output\n", + " node = json.loads(result.stdout)\n", + " assert node[\"name\"] == \"C\"\n", + " assert node[\"factory\"] == \"netrun.node_factories.from_function\"\n", + " assert node[\"factory_args\"][\"func\"] == \"my_module.my_func\"\n", + " # Check it was written\n", + " written = json.loads(Path(cfg).read_text())\n", + " names = [n[\"name\"] for n in written[\"graph\"][\"nodes\"]]\n", + " assert \"C\" in names\n", + "\n", + "\n", + "def test_add_node_raw_ports(tmp_path):\n", + " cfg = _make_config(tmp_path, _base_config())\n", + " result = runner.invoke(app, [\n", + " \"add-node\", \"C\", \"-c\", cfg,\n", + " \"--in-ports\", \"x,y\",\n", + " \"--out-ports\", \"result\",\n", + " \"--no-validate\",\n", + " ])\n", + " assert result.exit_code == 0, result.output\n", + " node = json.loads(result.stdout)\n", + " assert \"x\" in node[\"in_ports\"]\n", + " assert \"y\" in node[\"in_ports\"]\n", + " assert \"result\" in node[\"out_ports\"]\n", + "\n", + "\n", + "def test_add_node_json_stdin(tmp_path):\n", + " cfg = _make_config(tmp_path, _base_config())\n", + " node_json = json.dumps({\"in_ports\": {\"data\": {}}, \"out_ports\": {\"result\": {}}, \"factory\": \"some.factory\"})\n", + " result = runner.invoke(app, [\n", + " \"add-node\", \"C\", \"-c\", cfg, \"--json\", \"--no-validate\",\n", + " ], input=node_json)\n", + " assert result.exit_code == 0, result.output\n", + " node = json.loads(result.stdout)\n", + " assert node[\"name\"] == \"C\"\n", + " assert node[\"factory\"] == \"some.factory\"\n", + " assert \"data\" in node[\"in_ports\"]\n", + "\n", + "\n", + "def test_add_node_duplicate_error(tmp_path):\n", + " cfg = _make_config(tmp_path, _base_config())\n", + " result = runner.invoke(app, [\"add-node\", \"A\", \"-c\", cfg, \"--no-validate\"])\n", + " assert result.exit_code == 1\n", + " assert \"already exists\" in result.output\n", + "\n", + "\n", + "def test_add_node_custom_position(tmp_path):\n", + " cfg = _make_config(tmp_path, _base_config())\n", + " result = runner.invoke(app, [\n", + " \"add-node\", \"C\", \"-c\", cfg,\n", + " \"--position\", \"500,300\",\n", + " \"--no-validate\",\n", + " ])\n", + " assert result.exit_code == 0, result.output\n", + " node = json.loads(result.stdout)\n", + " assert node[\"extra\"][\"ui\"][\"position\"][\"x\"] == 500.0\n", + " assert node[\"extra\"][\"ui\"][\"position\"][\"y\"] == 300.0\n", + "\n", + "\n", + "def test_add_node_auto_position(tmp_path):\n", + " cfg = _make_config(tmp_path, _base_config())\n", + " result = runner.invoke(app, [\"add-node\", \"C\", \"-c\", cfg, \"--no-validate\"])\n", + " assert result.exit_code == 0, result.output\n", + " node = json.loads(result.stdout)\n", + " # max x is 400, so auto should be 700; avg y is 200\n", + " assert node[\"extra\"][\"ui\"][\"position\"][\"x\"] == 700.0\n", + " assert node[\"extra\"][\"ui\"][\"position\"][\"y\"] == 200.0" + ], + "execution_count": null, + "outputs": [], + "metadata": {} + }, + { + "cell_type": "markdown", + "source": [ + "## Test remove-node" + ], + "metadata": {} + }, + { + "cell_type": "code", + "source": [ + "#|export\n", + "def test_remove_node_with_edges(tmp_path):\n", + " cfg = _make_config(tmp_path, _base_config())\n", + " result = runner.invoke(app, [\"remove-node\", \"A\", \"-c\", cfg, \"--no-validate\"])\n", + " assert result.exit_code == 0, result.output\n", + " out = json.loads(result.stdout)\n", + " assert out[\"removed\"] == \"A\"\n", + " assert out[\"edges_removed\"] == 1\n", + " # Verify config\n", + " written = json.loads(Path(cfg).read_text())\n", + " names = [n[\"name\"] for n in written[\"graph\"][\"nodes\"]]\n", + " assert \"A\" not in names\n", + " assert len(written[\"graph\"][\"edges\"]) == 0\n", + "\n", + "\n", + "def test_remove_node_not_found(tmp_path):\n", + " cfg = _make_config(tmp_path, _base_config())\n", + " result = runner.invoke(app, [\"remove-node\", \"Z\", \"-c\", cfg, \"--no-validate\"])\n", + " assert result.exit_code == 1\n", + " assert \"not found\" in result.output" + ], + "execution_count": null, + "outputs": [], + "metadata": {} + }, + { + "cell_type": "markdown", + "source": [ + "## Test edit-node" + ], + "metadata": {} + }, + { + "cell_type": "code", + "source": [ + "#|export\n", + "def test_edit_node_rename(tmp_path):\n", + " cfg = _make_config(tmp_path, _base_config())\n", + " result = runner.invoke(app, [\"edit-node\", \"A\", \"-c\", cfg, \"--rename\", \"Alpha\", \"--no-validate\"])\n", + " assert result.exit_code == 0, result.output\n", + " node = json.loads(result.stdout)\n", + " assert node[\"name\"] == \"Alpha\"\n", + " # Verify edge refs updated\n", + " written = json.loads(Path(cfg).read_text())\n", + " edge = written[\"graph\"][\"edges\"][0]\n", + " assert edge[\"source_node\"] == \"Alpha\"\n", + "\n", + "\n", + "def test_edit_node_ports(tmp_path):\n", + " cfg = _make_config(tmp_path, _base_config())\n", + " result = runner.invoke(app, [\n", + " \"edit-node\", \"A\", \"-c\", cfg,\n", + " \"--add-in-port\", \"extra_in\",\n", + " \"--add-out-port\", \"extra_out\",\n", + " \"--remove-in-port\", \"in\",\n", + " \"--no-validate\",\n", + " ])\n", + " assert result.exit_code == 0, result.output\n", + " node = json.loads(result.stdout)\n", + " assert \"extra_in\" in node[\"in_ports\"]\n", + " assert \"extra_out\" in node[\"out_ports\"]\n", + " assert \"in\" not in node[\"in_ports\"]\n", + "\n", + "\n", + "def test_edit_node_merge(tmp_path):\n", + " cfg = _make_config(tmp_path, _base_config())\n", + " merge_json = json.dumps({\"execution_config\": {\"pools\": [\"main\"]}})\n", + " result = runner.invoke(app, [\n", + " \"edit-node\", \"A\", \"-c\", cfg,\n", + " \"--merge\", merge_json,\n", + " \"--no-validate\",\n", + " ])\n", + " assert result.exit_code == 0, result.output\n", + " node = json.loads(result.stdout)\n", + " assert node[\"execution_config\"][\"pools\"] == [\"main\"]\n", + " # Original fields preserved\n", + " assert node[\"name\"] == \"A\"\n", + " assert \"out\" in node[\"out_ports\"]\n", + "\n", + "\n", + "def test_edit_node_not_found(tmp_path):\n", + " cfg = _make_config(tmp_path, _base_config())\n", + " result = runner.invoke(app, [\"edit-node\", \"Z\", \"-c\", cfg, \"--rename\", \"ZZ\", \"--no-validate\"])\n", + " assert result.exit_code == 1\n", + " assert \"not found\" in result.output" + ], + "execution_count": null, + "outputs": [], + "metadata": {} + }, + { + "cell_type": "markdown", + "source": [ + "## Test add-edge" + ], + "metadata": {} + }, + { + "cell_type": "code", + "source": [ + "#|export\n", + "def test_add_edge_basic(tmp_path):\n", + " data = _base_config()\n", + " data[\"graph\"][\"edges\"] = [] # Start with no edges\n", + " cfg = _make_config(tmp_path, data)\n", + " result = runner.invoke(app, [\"add-edge\", \"A\", \"out\", \"B\", \"in\", \"-c\", cfg, \"--no-validate\"])\n", + " assert result.exit_code == 0, result.output\n", + " edge = json.loads(result.stdout)\n", + " assert edge[\"source_node\"] == \"A\"\n", + " assert edge[\"source_port\"] == \"out\"\n", + " assert edge[\"target_node\"] == \"B\"\n", + " assert edge[\"target_port\"] == \"in\"\n", + " # Verify written\n", + " written = json.loads(Path(cfg).read_text())\n", + " assert len(written[\"graph\"][\"edges\"]) == 1\n", + "\n", + "\n", + "def test_add_edge_dependency(tmp_path):\n", + " data = _base_config()\n", + " data[\"graph\"][\"edges\"] = []\n", + " cfg = _make_config(tmp_path, data)\n", + " result = runner.invoke(app, [\"add-edge\", \"A\", \"out\", \"B\", \"in\", \"-c\", cfg, \"--dependency\", \"--no-validate\"])\n", + " assert result.exit_code == 0, result.output\n", + " edge = json.loads(result.stdout)\n", + " assert edge[\"dependency\"] is True\n", + "\n", + "\n", + "def test_add_edge_fan_out_warning(tmp_path):\n", + " cfg = _make_config(tmp_path, _base_config())\n", + " # A.out already has an edge; adding another should warn\n", + " # First add a third node\n", + " runner.invoke(app, [\"add-node\", \"C\", \"-c\", cfg, \"--in-ports\", \"in\", \"--no-validate\"])\n", + " result = runner.invoke(app, [\"add-edge\", \"A\", \"out\", \"C\", \"in\", \"-c\", cfg, \"--no-validate\"])\n", + " assert result.exit_code == 0, result.output\n", + " assert \"fan-out\" in result.output.lower() or \"Fan-out\" in result.output\n", + "\n", + "\n", + "def test_add_edge_missing_node(tmp_path):\n", + " cfg = _make_config(tmp_path, _base_config())\n", + " result = runner.invoke(app, [\"add-edge\", \"Z\", \"out\", \"B\", \"in\", \"-c\", cfg, \"--no-validate\"])\n", + " assert result.exit_code == 1\n", + " assert \"not found\" in result.output" + ], + "execution_count": null, + "outputs": [], + "metadata": {} + }, + { + "cell_type": "markdown", + "source": [ + "## Test remove-edge" + ], + "metadata": {} + }, + { + "cell_type": "code", + "source": [ + "#|export\n", + "def test_remove_edge_basic(tmp_path):\n", + " cfg = _make_config(tmp_path, _base_config())\n", + " result = runner.invoke(app, [\"remove-edge\", \"A\", \"out\", \"B\", \"in\", \"-c\", cfg, \"--no-validate\"])\n", + " assert result.exit_code == 0, result.output\n", + " out = json.loads(result.stdout)\n", + " assert out[\"removed_source_node\"] == \"A\"\n", + " assert out[\"removed_source_port\"] == \"out\"\n", + " # Verify\n", + " written = json.loads(Path(cfg).read_text())\n", + " assert len(written[\"graph\"][\"edges\"]) == 0\n", + "\n", + "\n", + "def test_remove_edge_not_found(tmp_path):\n", + " cfg = _make_config(tmp_path, _base_config())\n", + " result = runner.invoke(app, [\"remove-edge\", \"X\", \"out\", \"Y\", \"in\", \"-c\", cfg, \"--no-validate\"])\n", + " assert result.exit_code == 1\n", + " assert \"not found\" in result.output" + ], + "execution_count": null, + "outputs": [], + "metadata": {} + }, + { + "cell_type": "markdown", + "source": [ + "## Test fan-out validation message" + ], + "metadata": {} + }, + { + "cell_type": "code", + "source": [ + "#|export\n", + "def test_validate_fan_out_suggestion():\n", + " from netrun.net.config._graph import GraphConfig\n", + " from netrun.net.config._nodes import NodeConfig, PortConfig, EdgeConfig\n", + "\n", + " config = GraphConfig(\n", + " nodes=[\n", + " NodeConfig(name=\"A\", out_ports={\"out\": PortConfig()}),\n", + " NodeConfig(name=\"B\", in_ports={\"in\": PortConfig()}),\n", + " NodeConfig(name=\"C\", in_ports={\"in\": PortConfig()}),\n", + " ],\n", + " edges=[\n", + " EdgeConfig(source_node=\"A\", source_port=\"out\", target_node=\"B\", target_port=\"in\"),\n", + " EdgeConfig(source_node=\"A\", source_port=\"out\", target_node=\"C\", target_port=\"in\"),\n", + " ],\n", + " )\n", + " errors = config.validate()\n", + " fan_out_errors = [e for e in errors if e.type == \"fan_out\"]\n", + " assert len(fan_out_errors) > 0\n", + " assert \"netrun.node_factories.broadcast\" in fan_out_errors[0].msg" + ], + "execution_count": null, + "outputs": [], + "metadata": {} + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/netrun/pts/netrun/10_cli/00_app.pct.py b/netrun-cli/pts/netrun_cli/00_app.pct.py similarity index 80% rename from netrun/pts/netrun/10_cli/00_app.pct.py rename to netrun-cli/pts/netrun_cli/00_app.pct.py index 0659ffca..5ffc1f4f 100644 --- a/netrun/pts/netrun/10_cli/00_app.pct.py +++ b/netrun-cli/pts/netrun_cli/00_app.pct.py @@ -7,7 +7,7 @@ # --- # %% -#|default_exp cli._app +#|default_exp _app # %% #|hide @@ -32,7 +32,7 @@ # %% #|export -from netrun.cli._commands import ( +from netrun_cli._commands import ( validate, structure, convert, @@ -40,6 +40,7 @@ info, nodes, node, + dry_run, ) app.command("validate")(validate) @@ -49,16 +50,17 @@ app.command("info")(info) app.command("nodes")(nodes) app.command("node")(node) +app.command("dry-run")(dry_run) # %% #|export -from netrun.cli._download_agents import download_agents +from netrun_cli._download_agents import download_agents app.command("download-agents")(download_agents) # %% #|export -from netrun.cli._graph import add_node, remove_node, edit_node, add_edge, remove_edge +from netrun_cli._graph import add_node, remove_node, edit_node, add_edge, remove_edge app.command("add-node")(add_node) app.command("remove-node")(remove_node) @@ -68,8 +70,8 @@ # %% #|export -from netrun.cli._actions import actions_app -from netrun.cli._recipes import recipes_app +from netrun_cli._actions import actions_app +from netrun_cli._recipes import recipes_app app.add_typer(actions_app, name="actions") app.add_typer(recipes_app, name="recipes") diff --git a/netrun/pts/netrun/10_cli/01_helpers.pct.py b/netrun-cli/pts/netrun_cli/01_helpers.pct.py similarity index 99% rename from netrun/pts/netrun/10_cli/01_helpers.pct.py rename to netrun-cli/pts/netrun_cli/01_helpers.pct.py index 9a72a24f..de16ccb1 100644 --- a/netrun/pts/netrun/10_cli/01_helpers.pct.py +++ b/netrun-cli/pts/netrun_cli/01_helpers.pct.py @@ -7,7 +7,7 @@ # --- # %% -#|default_exp cli._helpers +#|default_exp _helpers # %% #|hide diff --git a/netrun/pts/netrun/10_cli/02_commands.pct.py b/netrun-cli/pts/netrun_cli/02_commands.pct.py similarity index 73% rename from netrun/pts/netrun/10_cli/02_commands.pct.py rename to netrun-cli/pts/netrun_cli/02_commands.pct.py index e6a78d59..1687ef14 100644 --- a/netrun/pts/netrun/10_cli/02_commands.pct.py +++ b/netrun-cli/pts/netrun_cli/02_commands.pct.py @@ -7,7 +7,7 @@ # --- # %% -#|default_exp cli._commands +#|default_exp _commands # %% #|hide @@ -26,7 +26,7 @@ import typer from pydantic import ValidationError -from netrun.cli._helpers import ( +from netrun_cli._helpers import ( ConfigOpt, PrettyOpt, find_config, @@ -151,13 +151,36 @@ def _port_map(ports: dict) -> dict[str, str | None]: return {name: port_type_str(pc.port_type) for name, pc in ports.items()} +def _mermaid_id(name: str) -> str: + """Sanitise a node name for use as a Mermaid node ID.""" + return name.replace(" ", "_").replace("-", "_") + + def structure( config: ConfigOpt = None, + format: Annotated[str, typer.Option("--format", "-f", help="Output format: json or mermaid.")] = "json", pretty: PrettyOpt = True, ) -> None: - """Output graph topology as JSON (strips extra, execution_config, pools).""" + """Output graph topology (default JSON; use --format mermaid for Mermaid diagram).""" net_config, config_path = load_resolved_config(config) + if format == "mermaid": + lines = ["graph LR"] + for n in net_config.graph.nodes: + mid = _mermaid_id(n.name) + lines.append(f' {mid}["{n.name}"]') + for e in net_config.graph.edges: + src = _mermaid_id(e.source_node) + tgt = _mermaid_id(e.target_node) + label = f"{e.source_port} → {e.target_port}" + lines.append(f' {src} -->|"{label}"| {tgt}') + typer.echo("\n".join(lines)) + return + + if format != "json": + typer.echo(f"Error: unsupported format '{format}'. Use 'json' or 'mermaid'.", err=True) + raise typer.Exit(1) + nodes_out = [] for n in net_config.graph.nodes: if isinstance(n, SubgraphConfig): @@ -381,6 +404,7 @@ def nodes( def node( name: Annotated[str, typer.Argument(help="Node name.")], config: ConfigOpt = None, + edges: Annotated[bool, typer.Option("--edges", help="Include connected edges.")] = False, pretty: PrettyOpt = True, ) -> None: """Detailed info about a specific node.""" @@ -412,4 +436,111 @@ def node( if n.extra: result["extra"] = n.extra + if edges: + incoming = [] + outgoing = [] + for e in net_config.graph.edges: + if e.target_node == name: + incoming.append({"source": f"{e.source_node}.{e.source_port}", "port": e.target_port}) + if e.source_node == name: + outgoing.append({"port": e.source_port, "target": f"{e.target_node}.{e.target_port}"}) + result["edges"] = {"incoming": incoming, "outgoing": outgoing} + + output_json(result, pretty) + +# %% [markdown] +# ## dry-run + +# %% +#|export +def _topological_sort(nodes: list[str], edges: list[tuple[str, str]]) -> list[str]: + """Kahn's algorithm for topological sort. Returns nodes in execution order.""" + from collections import defaultdict, deque + + in_degree: dict[str, int] = {n: 0 for n in nodes} + adjacency: dict[str, list[str]] = defaultdict(list) + + for src, tgt in edges: + if src in in_degree and tgt in in_degree: + adjacency[src].append(tgt) + in_degree[tgt] += 1 + + queue = deque(n for n in nodes if in_degree[n] == 0) + result = [] + + while queue: + n = queue.popleft() + result.append(n) + for neighbor in adjacency[n]: + in_degree[neighbor] -= 1 + if in_degree[neighbor] == 0: + queue.append(neighbor) + + return result + + +def dry_run( + config: ConfigOpt = None, + pretty: PrettyOpt = True, +) -> None: + """Show execution plan without running node code. + + Displays topological order, source/sink nodes, pools, and scheduling constraints. + """ + net_config, _ = load_resolved_config(config) + + node_names = [n.name for n in net_config.graph.nodes] + + # Build data edges (non-dependency) for topological ordering + data_edges = [ + (e.source_node, e.target_node) + for e in net_config.graph.edges + if not e.dependency + ] + + # Include depends_on as edges for ordering + for n in net_config.graph.nodes: + if isinstance(n, SubgraphConfig): + continue + if n.execution_config and n.execution_config.depends_on: + for dep in n.execution_config.depends_on: + data_edges.append((dep, n.name)) + + topo_order = _topological_sort(node_names, data_edges) + + # Source nodes: no incoming data edges + targets = {tgt for _, tgt in data_edges} + source_nodes = [n for n in topo_order if n not in targets] + + # Sink nodes: no outgoing data edges + sources = {src for src, _ in data_edges} + sink_nodes = [n for n in topo_order if n not in sources] + + # Build execution order with metadata + execution_order = [] + for name in topo_order: + entry: dict[str, Any] = {"node": name} + + for n in net_config.graph.nodes: + if n.name == name and not isinstance(n, SubgraphConfig): + if n.execution_config: + ec = n.execution_config + if ec.pools: + entry["pools"] = ec.pools + if ec.depends_on: + entry["depends_on"] = ec.depends_on + if ec.resources: + entry["resources"] = ec.resources + break + + execution_order.append(entry) + + result = { + "source_nodes": source_nodes, + "sink_nodes": sink_nodes, + "execution_order": execution_order, + "total_nodes": len(node_names), + "total_edges": len(net_config.graph.edges), + } + output_json(result, pretty) diff --git a/netrun/pts/netrun/10_cli/03_actions.pct.py b/netrun-cli/pts/netrun_cli/03_actions.pct.py similarity index 97% rename from netrun/pts/netrun/10_cli/03_actions.pct.py rename to netrun-cli/pts/netrun_cli/03_actions.pct.py index 844c324e..96a67c30 100644 --- a/netrun/pts/netrun/10_cli/03_actions.pct.py +++ b/netrun-cli/pts/netrun_cli/03_actions.pct.py @@ -7,7 +7,7 @@ # --- # %% -#|default_exp cli._actions +#|default_exp _actions # %% #|hide @@ -20,7 +20,7 @@ import typer -from netrun.cli._helpers import ConfigOpt, PrettyOpt, load_config, output_json, get_node_by_name +from netrun_cli._helpers import ConfigOpt, PrettyOpt, load_config, output_json, get_node_by_name from netrun.tools._helpers import ( get_available_actions, build_action_context, diff --git a/netrun/pts/netrun/10_cli/04_recipes.pct.py b/netrun-cli/pts/netrun_cli/04_recipes.pct.py similarity index 97% rename from netrun/pts/netrun/10_cli/04_recipes.pct.py rename to netrun-cli/pts/netrun_cli/04_recipes.pct.py index f3769769..43896a89 100644 --- a/netrun/pts/netrun/10_cli/04_recipes.pct.py +++ b/netrun-cli/pts/netrun_cli/04_recipes.pct.py @@ -7,7 +7,7 @@ # --- # %% -#|default_exp cli._recipes +#|default_exp _recipes # %% #|hide @@ -21,7 +21,7 @@ import typer -from netrun.cli._helpers import ConfigOpt, PrettyOpt, load_raw_data, output_json +from netrun_cli._helpers import ConfigOpt, PrettyOpt, load_raw_data, output_json from netrun.tools._helpers import get_recipes from netrun.tools._recipes import execute_recipe, get_recipe_prompts diff --git a/netrun/pts/netrun/10_cli/05_download_agents.pct.py b/netrun-cli/pts/netrun_cli/05_download_agents.pct.py similarity index 98% rename from netrun/pts/netrun/10_cli/05_download_agents.pct.py rename to netrun-cli/pts/netrun_cli/05_download_agents.pct.py index adf77196..89133df8 100644 --- a/netrun/pts/netrun/10_cli/05_download_agents.pct.py +++ b/netrun-cli/pts/netrun_cli/05_download_agents.pct.py @@ -7,7 +7,7 @@ # --- # %% -#|default_exp cli._download_agents +#|default_exp _download_agents # %% #|hide diff --git a/netrun/pts/netrun/10_cli/06_graph.pct.py b/netrun-cli/pts/netrun_cli/06_graph.pct.py similarity index 99% rename from netrun/pts/netrun/10_cli/06_graph.pct.py rename to netrun-cli/pts/netrun_cli/06_graph.pct.py index 421e4e62..885504a4 100644 --- a/netrun/pts/netrun/10_cli/06_graph.pct.py +++ b/netrun-cli/pts/netrun_cli/06_graph.pct.py @@ -7,7 +7,7 @@ # --- # %% -#|default_exp cli._graph +#|default_exp _graph # %% #|hide @@ -22,7 +22,7 @@ import typer -from netrun.cli._helpers import ( +from netrun_cli._helpers import ( ConfigOpt, PrettyOpt, find_config, diff --git a/netrun/pts/tests/10_cli/__init__.py b/netrun-cli/pts/tests/__init__.py similarity index 100% rename from netrun/pts/tests/10_cli/__init__.py rename to netrun-cli/pts/tests/__init__.py diff --git a/netrun/pts/tests/10_cli/test_cli.pct.py b/netrun-cli/pts/tests/test_cli.pct.py similarity index 70% rename from netrun/pts/tests/10_cli/test_cli.pct.py rename to netrun-cli/pts/tests/test_cli.pct.py index 2379f78e..5c9e1771 100644 --- a/netrun/pts/tests/10_cli/test_cli.pct.py +++ b/netrun-cli/pts/tests/test_cli.pct.py @@ -7,7 +7,7 @@ # --- # %% -#|default_exp cli.test_cli +#|default_exp test_cli # %% #|hide @@ -22,13 +22,13 @@ import pytest from typer.testing import CliRunner -from netrun.cli._app import app +from netrun_cli._app import app # %% #|export runner = CliRunner() -SAMPLE_DIR = Path(__file__).resolve().parent.parent.parent.parent.parent / "sample_projects" +SAMPLE_DIR = Path(__file__).resolve().parent.parent.parent.parent / "sample_projects" BASIC_CONFIG = str(SAMPLE_DIR / "00_basic_net_project" / "main.netrun.json") POOLS_CONFIG = str(SAMPLE_DIR / "01_thread_and_process_pools" / "main.netrun.json") @@ -302,7 +302,7 @@ def test_download_agents_success(tmp_path): mock_fn = _make_mock_urlopen(tree, file_contents) out_dir = str(tmp_path / "agents") - with patch("netrun.cli._download_agents.urllib.request.urlopen", side_effect=mock_fn): + with patch("netrun_cli._download_agents.urllib.request.urlopen", side_effect=mock_fn): result = runner.invoke(app, ["download-agents", out_dir]) assert result.exit_code == 0 @@ -319,7 +319,7 @@ def test_download_agents_no_files(tmp_path): mock_fn = _make_mock_urlopen(tree) out_dir = str(tmp_path / "agents") - with patch("netrun.cli._download_agents.urllib.request.urlopen", side_effect=mock_fn): + with patch("netrun_cli._download_agents.urllib.request.urlopen", side_effect=mock_fn): result = runner.invoke(app, ["download-agents", out_dir]) assert result.exit_code == 1 @@ -330,7 +330,7 @@ def test_download_agents_network_error(tmp_path): out_dir = str(tmp_path / "agents") with patch( - "netrun.cli._download_agents.urllib.request.urlopen", + "netrun_cli._download_agents.urllib.request.urlopen", side_effect=urllib.error.URLError("connection refused"), ): result = runner.invoke(app, ["download-agents", out_dir]) @@ -344,7 +344,7 @@ def test_download_agents_branch_option(tmp_path): mock_fn = _make_mock_urlopen(tree, {"agents/INSTRUCTIONS.md": b"instructions"}) out_dir = str(tmp_path / "agents") - with patch("netrun.cli._download_agents.urllib.request.urlopen", side_effect=mock_fn) as _: + with patch("netrun_cli._download_agents.urllib.request.urlopen", side_effect=mock_fn) as _: result = runner.invoke(app, ["download-agents", out_dir, "-b", "dev"]) assert result.exit_code == 0 @@ -384,7 +384,7 @@ def mock_urlopen(req_or_url, **kwargs): out_dir = str(tmp_path / "agents") - with patch("netrun.cli._download_agents.urllib.request.urlopen", side_effect=mock_urlopen): + with patch("netrun_cli._download_agents.urllib.request.urlopen", side_effect=mock_urlopen): result = runner.invoke(app, ["download-agents", out_dir]) assert result.exit_code == 0 @@ -410,6 +410,7 @@ def test_help(): assert "actions" in result.stdout assert "recipes" in result.stdout assert "download-agents" in result.stdout + assert "dry-run" in result.stdout # %% [markdown] # ## Regression: validate reports resolution failures @@ -458,3 +459,156 @@ def test_validate_reports_resolution_failure(): "resolution" in str(m).lower() or "module" in str(m).lower() for m in all_messages ), f"validate silently swallowed resolution failure: {data}" + +# %% [markdown] +# ## Test node --edges + +# %% +#|export +def test_node_edges_incoming(): + result = runner.invoke(app, ["node", "add", "--edges", "-c", BASIC_CONFIG, "--pretty"]) + assert result.exit_code == 0 + data = json.loads(result.stdout) + assert "edges" in data + assert "incoming" in data["edges"] + assert "outgoing" in data["edges"] + # "add" has an incoming edge from "double" + incoming = data["edges"]["incoming"] + assert len(incoming) >= 1 + assert any(e["source"] == "double.out" and e["port"] == "a" for e in incoming) + + +def test_node_edges_outgoing(): + result = runner.invoke(app, ["node", "double", "--edges", "-c", BASIC_CONFIG, "--pretty"]) + assert result.exit_code == 0 + data = json.loads(result.stdout) + outgoing = data["edges"]["outgoing"] + assert len(outgoing) == 1 + assert outgoing[0]["port"] == "out" + assert outgoing[0]["target"] == "add.a" + + +def test_node_edges_none(): + """Node with no connected edges returns empty lists.""" + result = runner.invoke(app, ["node", "analyze", "--edges", "-c", BASIC_CONFIG, "--pretty"]) + assert result.exit_code == 0 + data = json.loads(result.stdout) + assert data["edges"]["incoming"] == [] + assert data["edges"]["outgoing"] == [] + + +def test_node_no_edges_by_default(): + result = runner.invoke(app, ["node", "add", "-c", BASIC_CONFIG, "--pretty"]) + assert result.exit_code == 0 + data = json.loads(result.stdout) + assert "edges" not in data + +# %% [markdown] +# ## Test structure --format mermaid + +# %% +#|export +def test_structure_mermaid(): + result = runner.invoke(app, ["structure", "-c", BASIC_CONFIG, "--format", "mermaid"]) + assert result.exit_code == 0 + lines = result.stdout.strip().split("\n") + assert lines[0] == "graph LR" + # Node declarations + assert any('double["double"]' in line for line in lines) + assert any('add["add"]' in line for line in lines) + # Edge with label + assert any('double -->|"out → a"| add' in line for line in lines) + + +def test_structure_json_default(): + """Existing JSON output still works without --format.""" + result = runner.invoke(app, ["structure", "-c", BASIC_CONFIG]) + assert result.exit_code == 0 + data = json.loads(result.stdout) + assert "nodes" in data + assert "edges" in data + + +def test_structure_bad_format(): + result = runner.invoke(app, ["structure", "-c", BASIC_CONFIG, "--format", "xml"]) + assert result.exit_code == 1 + +# %% [markdown] +# ## Test dry-run + +# %% +#|export +def test_dry_run_basic(): + result = runner.invoke(app, ["dry-run", "-c", BASIC_CONFIG, "--pretty"]) + assert result.exit_code == 0 + data = json.loads(result.stdout) + assert "source_nodes" in data + assert "sink_nodes" in data + assert "execution_order" in data + assert data["total_nodes"] == 4 + assert data["total_edges"] == 2 + # double and analyze have no incoming edges → source nodes + assert "double" in data["source_nodes"] + assert "analyze" in data["source_nodes"] + + +def test_dry_run_execution_order(): + result = runner.invoke(app, ["dry-run", "-c", BASIC_CONFIG]) + assert result.exit_code == 0 + data = json.loads(result.stdout) + order = [e["node"] for e in data["execution_order"]] + # double must come before add (double→add edge), add before format_result + assert order.index("double") < order.index("add") + assert order.index("add") < order.index("format_result") + + +def test_dry_run_sink_nodes(): + result = runner.invoke(app, ["dry-run", "-c", BASIC_CONFIG]) + assert result.exit_code == 0 + data = json.loads(result.stdout) + # format_result and analyze have no outgoing data edges + assert "format_result" in data["sink_nodes"] + assert "analyze" in data["sink_nodes"] + + +def test_dry_run_depends_on_and_resources(tmp_path): + """Test dry-run output includes depends_on and resources metadata.""" + config_data = { + "graph": { + "nodes": [ + { + "name": "A", + "in_ports": {"in": {}}, + "out_ports": {"out": {}}, + "execution_config": {"pools": ["main"]}, + }, + { + "name": "B", + "in_ports": {"in": {}}, + "out_ports": {"out": {}}, + "execution_config": { + "pools": ["main"], + "depends_on": ["A"], + "resources": {"gpu": 1}, + }, + }, + ], + "edges": [], + }, + "pools": {"main": {"spec": {"type": "main"}}}, + } + config_path = tmp_path / "test.netrun.json" + config_path.write_text(json.dumps(config_data)) + + result = runner.invoke(app, ["dry-run", "-c", str(config_path), "--pretty"]) + assert result.exit_code == 0 + data = json.loads(result.stdout) + + order = [e["node"] for e in data["execution_order"]] + # A must come before B because of depends_on + assert order.index("A") < order.index("B") + + # B should have depends_on and resources in its entry + b_entry = next(e for e in data["execution_order"] if e["node"] == "B") + assert b_entry["depends_on"] == ["A"] + assert b_entry["resources"] == {"gpu": 1} diff --git a/netrun/pts/tests/10_cli/test_cli_graph.pct.py b/netrun-cli/pts/tests/test_cli_graph.pct.py similarity index 99% rename from netrun/pts/tests/10_cli/test_cli_graph.pct.py rename to netrun-cli/pts/tests/test_cli_graph.pct.py index ac7d9161..2483bdde 100644 --- a/netrun/pts/tests/10_cli/test_cli_graph.pct.py +++ b/netrun-cli/pts/tests/test_cli_graph.pct.py @@ -7,7 +7,7 @@ # --- # %% -#|default_exp cli.test_cli_graph +#|default_exp test_cli_graph # %% #|hide @@ -21,7 +21,7 @@ import pytest from typer.testing import CliRunner -from netrun.cli._app import app +from netrun_cli._app import app # %% #|export diff --git a/netrun-cli/pyproject.toml b/netrun-cli/pyproject.toml new file mode 100644 index 00000000..e627e3a3 --- /dev/null +++ b/netrun-cli/pyproject.toml @@ -0,0 +1,82 @@ +[project] +name = "netrun-cli" +version = "0.5.0" +description = "CLI for inspecting and managing netrun projects." +requires-python = ">=3.11" +dependencies = [ + "netrun>=0.5.0", + "typer>=0.15.0", + "tomli-w>=1.0", +] + +[project.scripts] +netrun = "netrun_cli._app:app_main" + +[project.urls] +Homepage = "https://github.com/lukastk/netrun" +Repository = "https://github.com/lukastk/netrun" + +[dependency-groups] +dev = [ + "nblite>=1.1.5", + "pytest>=9.0.2", + "ruff>=0.8.0", +] + +[tool.uv.sources] +netrun = { path = "../netrun", editable = true } +netrun-sim = { path = "../netrun-sim/python", editable = true } + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build] +ignore-vcs = true +sources = ["src"] + +[tool.ruff] +target-version = "py311" +line-length = 120 +extend-exclude = [ + "__*.pct.py", + "*.ipynb", +] + +[tool.ruff.lint] +select = [ + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # pyflakes + "I", # isort + "B", # flake8-bugbear + "C4", # flake8-comprehensions + "UP", # pyupgrade +] +ignore = [ + "E501", # line too long (handled by formatter) + "B008", # do not perform function calls in argument defaults + "E402", # Module level import not at top +] + +[tool.ruff.lint.per-file-ignores] +"*.pct.py" = [ + "I001", # Unsorted imports + "E701", # Multiple statements on one line newline + "E702", # Multiple statements on one line semicolon + "E703", # useless-semicolon +] + +[tool.ruff.lint.isort] +known-first-party = ["nblite"] + +[tool.ruff.format] +quote-style = "double" +indent-style = "space" + +[tool.pytest.ini_options] +testpaths = ["src/tests"] +python_files = ["test_*.py"] +python_functions = ["test_*"] +addopts = "-v --tb=short" +pythonpath = ["src"] diff --git a/netrun-cli/src/netrun_cli/__init__.py b/netrun-cli/src/netrun_cli/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/netrun/src/netrun/cli/_actions.py b/netrun-cli/src/netrun_cli/_actions.py similarity index 89% rename from netrun/src/netrun/cli/_actions.py rename to netrun-cli/src/netrun_cli/_actions.py index 34519dde..7b5f94f0 100644 --- a/netrun/src/netrun/cli/_actions.py +++ b/netrun-cli/src/netrun_cli/_actions.py @@ -1,22 +1,22 @@ -# AUTOGENERATED! DO NOT EDIT! File to edit: pts/netrun/10_cli/03_actions.pct.py +# AUTOGENERATED! DO NOT EDIT! File to edit: pts/netrun_cli/03_actions.pct.py __all__ = ['NodeOpt', 'actions_app', 'actions_list', 'actions_run'] -# %% pts/netrun/10_cli/03_actions.pct.py 2 +# %% pts/netrun_cli/03_actions.pct.py 2 import asyncio from typing import Annotated, Optional import typer -from ..cli._helpers import ConfigOpt, PrettyOpt, load_config, output_json, get_node_by_name -from ..tools._helpers import ( +from ._helpers import ConfigOpt, PrettyOpt, load_config, output_json, get_node_by_name +from netrun.tools._helpers import ( get_available_actions, build_action_context, ) -from ..tools._execute import execute_action -from ..tools._models import ActionConfig +from netrun.tools._execute import execute_action +from netrun.tools._models import ActionConfig -# %% pts/netrun/10_cli/03_actions.pct.py 4 +# %% pts/netrun_cli/03_actions.pct.py 4 actions_app = typer.Typer(help="List and run actions.", no_args_is_help=True) NodeOpt = Annotated[Optional[str], typer.Option("--node", "-n", help="Node name for node-level actions.")] diff --git a/netrun/src/netrun/cli/_app.py b/netrun-cli/src/netrun_cli/_app.py similarity index 60% rename from netrun/src/netrun/cli/_app.py rename to netrun-cli/src/netrun_cli/_app.py index 17122a1b..532b3080 100644 --- a/netrun/src/netrun/cli/_app.py +++ b/netrun-cli/src/netrun_cli/_app.py @@ -1,19 +1,19 @@ -# AUTOGENERATED! DO NOT EDIT! File to edit: pts/netrun/10_cli/00_app.pct.py +# AUTOGENERATED! DO NOT EDIT! File to edit: pts/netrun_cli/00_app.pct.py __all__ = ['app', 'app_main'] -# %% pts/netrun/10_cli/00_app.pct.py 2 +# %% pts/netrun_cli/00_app.pct.py 2 import typer -# %% pts/netrun/10_cli/00_app.pct.py 4 +# %% pts/netrun_cli/00_app.pct.py 4 app = typer.Typer( name="netrun", help="CLI for inspecting and managing netrun projects.", no_args_is_help=True, ) -# %% pts/netrun/10_cli/00_app.pct.py 5 -from ..cli._commands import ( +# %% pts/netrun_cli/00_app.pct.py 5 +from ._commands import ( validate, structure, convert, @@ -21,6 +21,7 @@ info, nodes, node, + dry_run, ) app.command("validate")(validate) @@ -30,14 +31,15 @@ app.command("info")(info) app.command("nodes")(nodes) app.command("node")(node) +app.command("dry-run")(dry_run) -# %% pts/netrun/10_cli/00_app.pct.py 6 -from ..cli._download_agents import download_agents +# %% pts/netrun_cli/00_app.pct.py 6 +from ._download_agents import download_agents app.command("download-agents")(download_agents) -# %% pts/netrun/10_cli/00_app.pct.py 7 -from ..cli._graph import add_node, remove_node, edit_node, add_edge, remove_edge +# %% pts/netrun_cli/00_app.pct.py 7 +from ._graph import add_node, remove_node, edit_node, add_edge, remove_edge app.command("add-node")(add_node) app.command("remove-node")(remove_node) @@ -45,14 +47,14 @@ app.command("add-edge")(add_edge) app.command("remove-edge")(remove_edge) -# %% pts/netrun/10_cli/00_app.pct.py 8 -from ..cli._actions import actions_app -from ..cli._recipes import recipes_app +# %% pts/netrun_cli/00_app.pct.py 8 +from ._actions import actions_app +from ._recipes import recipes_app app.add_typer(actions_app, name="actions") app.add_typer(recipes_app, name="recipes") -# %% pts/netrun/10_cli/00_app.pct.py 9 +# %% pts/netrun_cli/00_app.pct.py 9 def app_main() -> None: """Entry point for the netrun CLI.""" app() diff --git a/netrun/src/netrun/cli/_commands.py b/netrun-cli/src/netrun_cli/_commands.py similarity index 69% rename from netrun/src/netrun/cli/_commands.py rename to netrun-cli/src/netrun_cli/_commands.py index c30a4d05..fb2308c4 100644 --- a/netrun/src/netrun/cli/_commands.py +++ b/netrun-cli/src/netrun_cli/_commands.py @@ -1,8 +1,8 @@ -# AUTOGENERATED! DO NOT EDIT! File to edit: pts/netrun/10_cli/02_commands.pct.py +# AUTOGENERATED! DO NOT EDIT! File to edit: pts/netrun_cli/02_commands.pct.py -__all__ = ['convert', 'factory_info', 'info', 'node', 'nodes', 'structure', 'validate'] +__all__ = ['convert', 'dry_run', 'factory_info', 'info', 'node', 'nodes', 'structure', 'validate'] -# %% pts/netrun/10_cli/02_commands.pct.py 2 +# %% pts/netrun_cli/02_commands.pct.py 2 import importlib import inspect import json @@ -14,7 +14,7 @@ import typer from pydantic import ValidationError -from ..cli._helpers import ( +from ._helpers import ( ConfigOpt, PrettyOpt, find_config, @@ -25,9 +25,9 @@ get_node_by_name, port_type_str, ) -from ..net.config._nodes import NodeConfig, SubgraphConfig +from netrun.net.config._nodes import NodeConfig, SubgraphConfig -# %% pts/netrun/10_cli/02_commands.pct.py 5 +# %% pts/netrun_cli/02_commands.pct.py 5 def validate( config: ConfigOpt = None, pretty: PrettyOpt = True, @@ -41,7 +41,7 @@ def validate( # Step 1: Pydantic validation — must succeed to continue try: - from ..net.config._net_config import NetConfig + from netrun.net.config._net_config import NetConfig net_config = NetConfig.from_file(config_path) node_count = len(net_config.graph.nodes) edge_count = len(net_config.graph.edges) @@ -122,19 +122,42 @@ def _check_actions(actions: list[dict], prefix: str) -> None: result["warnings"] = warnings output_json(result, pretty) -# %% pts/netrun/10_cli/02_commands.pct.py 7 +# %% pts/netrun_cli/02_commands.pct.py 7 def _port_map(ports: dict) -> dict[str, str | None]: """Convert port dict to {name: type_str}.""" return {name: port_type_str(pc.port_type) for name, pc in ports.items()} +def _mermaid_id(name: str) -> str: + """Sanitise a node name for use as a Mermaid node ID.""" + return name.replace(" ", "_").replace("-", "_") + + def structure( config: ConfigOpt = None, + format: Annotated[str, typer.Option("--format", "-f", help="Output format: json or mermaid.")] = "json", pretty: PrettyOpt = True, ) -> None: - """Output graph topology as JSON (strips extra, execution_config, pools).""" + """Output graph topology (default JSON; use --format mermaid for Mermaid diagram).""" net_config, config_path = load_resolved_config(config) + if format == "mermaid": + lines = ["graph LR"] + for n in net_config.graph.nodes: + mid = _mermaid_id(n.name) + lines.append(f' {mid}["{n.name}"]') + for e in net_config.graph.edges: + src = _mermaid_id(e.source_node) + tgt = _mermaid_id(e.target_node) + label = f"{e.source_port} → {e.target_port}" + lines.append(f' {src} -->|"{label}"| {tgt}') + typer.echo("\n".join(lines)) + return + + if format != "json": + typer.echo(f"Error: unsupported format '{format}'. Use 'json' or 'mermaid'.", err=True) + raise typer.Exit(1) + nodes_out = [] for n in net_config.graph.nodes: if isinstance(n, SubgraphConfig): @@ -167,7 +190,7 @@ def structure( output_json({"nodes": nodes_out, "edges": edges_out}, pretty) -# %% pts/netrun/10_cli/02_commands.pct.py 9 +# %% pts/netrun_cli/02_commands.pct.py 9 def convert( config_file: Annotated[str, typer.Argument(help="Path to config file to convert.")], output: Annotated[Optional[str], typer.Option("--output", "-o", help="Output file path.")] = None, @@ -208,7 +231,7 @@ def convert( else: typer.echo(result) -# %% pts/netrun/10_cli/02_commands.pct.py 11 +# %% pts/netrun_cli/02_commands.pct.py 11 def factory_info( factory_path: Annotated[str, typer.Argument(help="Dotted import path to factory module.")], pretty: PrettyOpt = True, @@ -255,7 +278,7 @@ def factory_info( output_json(result, pretty) -# %% pts/netrun/10_cli/02_commands.pct.py 13 +# %% pts/netrun_cli/02_commands.pct.py 13 def info( config: ConfigOpt = None, pretty: PrettyOpt = True, @@ -310,7 +333,7 @@ def info( output_json(result, pretty) -# %% pts/netrun/10_cli/02_commands.pct.py 15 +# %% pts/netrun_cli/02_commands.pct.py 15 def nodes( config: ConfigOpt = None, pretty: PrettyOpt = True, @@ -334,10 +357,11 @@ def nodes( output_json(result, pretty) -# %% pts/netrun/10_cli/02_commands.pct.py 17 +# %% pts/netrun_cli/02_commands.pct.py 17 def node( name: Annotated[str, typer.Argument(help="Node name.")], config: ConfigOpt = None, + edges: Annotated[bool, typer.Option("--edges", help="Include connected edges.")] = False, pretty: PrettyOpt = True, ) -> None: """Detailed info about a specific node.""" @@ -369,4 +393,107 @@ def node( if n.extra: result["extra"] = n.extra + if edges: + incoming = [] + outgoing = [] + for e in net_config.graph.edges: + if e.target_node == name: + incoming.append({"source": f"{e.source_node}.{e.source_port}", "port": e.target_port}) + if e.source_node == name: + outgoing.append({"port": e.source_port, "target": f"{e.target_node}.{e.target_port}"}) + result["edges"] = {"incoming": incoming, "outgoing": outgoing} + + output_json(result, pretty) + +# %% pts/netrun_cli/02_commands.pct.py 19 +def _topological_sort(nodes: list[str], edges: list[tuple[str, str]]) -> list[str]: + """Kahn's algorithm for topological sort. Returns nodes in execution order.""" + from collections import defaultdict, deque + + in_degree: dict[str, int] = {n: 0 for n in nodes} + adjacency: dict[str, list[str]] = defaultdict(list) + + for src, tgt in edges: + if src in in_degree and tgt in in_degree: + adjacency[src].append(tgt) + in_degree[tgt] += 1 + + queue = deque(n for n in nodes if in_degree[n] == 0) + result = [] + + while queue: + n = queue.popleft() + result.append(n) + for neighbor in adjacency[n]: + in_degree[neighbor] -= 1 + if in_degree[neighbor] == 0: + queue.append(neighbor) + + return result + + +def dry_run( + config: ConfigOpt = None, + pretty: PrettyOpt = True, +) -> None: + """Show execution plan without running node code. + + Displays topological order, source/sink nodes, pools, and scheduling constraints. + """ + net_config, _ = load_resolved_config(config) + + node_names = [n.name for n in net_config.graph.nodes] + + # Build data edges (non-dependency) for topological ordering + data_edges = [ + (e.source_node, e.target_node) + for e in net_config.graph.edges + if not e.dependency + ] + + # Include depends_on as edges for ordering + for n in net_config.graph.nodes: + if isinstance(n, SubgraphConfig): + continue + if n.execution_config and n.execution_config.depends_on: + for dep in n.execution_config.depends_on: + data_edges.append((dep, n.name)) + + topo_order = _topological_sort(node_names, data_edges) + + # Source nodes: no incoming data edges + targets = {tgt for _, tgt in data_edges} + source_nodes = [n for n in topo_order if n not in targets] + + # Sink nodes: no outgoing data edges + sources = {src for src, _ in data_edges} + sink_nodes = [n for n in topo_order if n not in sources] + + # Build execution order with metadata + execution_order = [] + for name in topo_order: + entry: dict[str, Any] = {"node": name} + + for n in net_config.graph.nodes: + if n.name == name and not isinstance(n, SubgraphConfig): + if n.execution_config: + ec = n.execution_config + if ec.pools: + entry["pools"] = ec.pools + if ec.depends_on: + entry["depends_on"] = ec.depends_on + if ec.resources: + entry["resources"] = ec.resources + break + + execution_order.append(entry) + + result = { + "source_nodes": source_nodes, + "sink_nodes": sink_nodes, + "execution_order": execution_order, + "total_nodes": len(node_names), + "total_edges": len(net_config.graph.edges), + } + output_json(result, pretty) diff --git a/netrun/src/netrun/cli/_download_agents.py b/netrun-cli/src/netrun_cli/_download_agents.py similarity index 92% rename from netrun/src/netrun/cli/_download_agents.py rename to netrun-cli/src/netrun_cli/_download_agents.py index 0c507aa0..d57f9299 100644 --- a/netrun/src/netrun/cli/_download_agents.py +++ b/netrun-cli/src/netrun_cli/_download_agents.py @@ -1,8 +1,8 @@ -# AUTOGENERATED! DO NOT EDIT! File to edit: pts/netrun/10_cli/05_download_agents.pct.py +# AUTOGENERATED! DO NOT EDIT! File to edit: pts/netrun_cli/05_download_agents.pct.py __all__ = ['download_agents'] -# %% pts/netrun/10_cli/05_download_agents.pct.py 2 +# %% pts/netrun_cli/05_download_agents.pct.py 2 import json import urllib.request import urllib.error @@ -11,7 +11,7 @@ import typer -# %% pts/netrun/10_cli/05_download_agents.pct.py 4 +# %% pts/netrun_cli/05_download_agents.pct.py 4 _REPO_OWNER = "lukastk" _REPO_NAME = "netrun" diff --git a/netrun/src/netrun/cli/_graph.py b/netrun-cli/src/netrun_cli/_graph.py similarity index 97% rename from netrun/src/netrun/cli/_graph.py rename to netrun-cli/src/netrun_cli/_graph.py index 7508ac6e..f7bc00b0 100644 --- a/netrun/src/netrun/cli/_graph.py +++ b/netrun-cli/src/netrun_cli/_graph.py @@ -1,8 +1,8 @@ -# AUTOGENERATED! DO NOT EDIT! File to edit: pts/netrun/10_cli/06_graph.pct.py +# AUTOGENERATED! DO NOT EDIT! File to edit: pts/netrun_cli/06_graph.pct.py __all__ = ['NoValidateOpt', 'add_edge', 'add_node', 'edit_node', 'remove_edge', 'remove_node'] -# %% pts/netrun/10_cli/06_graph.pct.py 2 +# %% pts/netrun_cli/06_graph.pct.py 2 import json import sys from pathlib import Path @@ -10,7 +10,7 @@ import typer -from ..cli._helpers import ( +from ._helpers import ( ConfigOpt, PrettyOpt, find_config, @@ -22,7 +22,7 @@ validate_after_write, ) -# %% pts/netrun/10_cli/06_graph.pct.py 5 +# %% pts/netrun_cli/06_graph.pct.py 5 NoValidateOpt = Annotated[bool, typer.Option("--no-validate", help="Skip post-write validation.")] @@ -95,7 +95,7 @@ def add_node( output_json(node_dict, pretty) -# %% pts/netrun/10_cli/06_graph.pct.py 7 +# %% pts/netrun_cli/06_graph.pct.py 7 def _edge_references_node(edge: dict, node_name: str) -> bool: """Check if an edge references a node.""" if edge.get("source_node") == node_name: @@ -165,7 +165,7 @@ def _port_ref_matches_node(port_ref: Any, node_name: str) -> bool: return port_ref.get("node_name") == node_name return False -# %% pts/netrun/10_cli/06_graph.pct.py 9 +# %% pts/netrun_cli/06_graph.pct.py 9 def _update_edge_node_refs(edges: list[dict], old_name: str, new_name: str) -> None: """Rename node references in edges.""" for edge in edges: @@ -273,7 +273,7 @@ def edit_node( output_json(node_dict, pretty) -# %% pts/netrun/10_cli/06_graph.pct.py 11 +# %% pts/netrun_cli/06_graph.pct.py 11 def add_edge( source_node_arg: Annotated[str, typer.Argument(help="Source node name.")], source_port_arg: Annotated[str, typer.Argument(help="Source port name.")], @@ -326,7 +326,7 @@ def add_edge( output_json(edge_dict, pretty) -# %% pts/netrun/10_cli/06_graph.pct.py 13 +# %% pts/netrun_cli/06_graph.pct.py 13 def _edge_matches(edge: dict, source_node: str, source_port: str, target_node: str, target_port: str) -> bool: """Check if an edge matches the given source and target fields.""" return ( diff --git a/netrun/src/netrun/cli/_helpers.py b/netrun-cli/src/netrun_cli/_helpers.py similarity index 95% rename from netrun/src/netrun/cli/_helpers.py rename to netrun-cli/src/netrun_cli/_helpers.py index d1b7d212..9b1a534e 100644 --- a/netrun/src/netrun/cli/_helpers.py +++ b/netrun-cli/src/netrun_cli/_helpers.py @@ -1,8 +1,8 @@ -# AUTOGENERATED! DO NOT EDIT! File to edit: pts/netrun/10_cli/01_helpers.pct.py +# AUTOGENERATED! DO NOT EDIT! File to edit: pts/netrun_cli/01_helpers.pct.py __all__ = ['ConfigOpt', 'NETRUN_EXTENSIONS', 'PrettyOpt', 'auto_position', 'deep_merge', 'find_config', 'get_node_by_name', 'load_config', 'load_raw_data', 'load_resolved_config', 'output_json', 'port_type_str', 'validate_after_write', 'write_config_data'] -# %% pts/netrun/10_cli/01_helpers.pct.py 2 +# %% pts/netrun_cli/01_helpers.pct.py 2 import json import sys import tomllib @@ -11,14 +11,14 @@ import typer -from ..net.config._net_config import NetConfig -from ..net.config._nodes import NodeConfig +from netrun.net.config._net_config import NetConfig +from netrun.net.config._nodes import NodeConfig -# %% pts/netrun/10_cli/01_helpers.pct.py 4 +# %% pts/netrun_cli/01_helpers.pct.py 4 ConfigOpt = Annotated[Optional[str], typer.Option("--config", "-c", help="Path to netrun config file.")] PrettyOpt = Annotated[bool, typer.Option("--pretty/--compact", help="Pretty-print or compact JSON output.")] -# %% pts/netrun/10_cli/01_helpers.pct.py 5 +# %% pts/netrun_cli/01_helpers.pct.py 5 NETRUN_EXTENSIONS = (".netrun.json", ".netrun.toml") @@ -157,7 +157,7 @@ def port_type_str(port_type: Any) -> str | None: return port_type.name return str(port_type) -# %% pts/netrun/10_cli/01_helpers.pct.py 6 +# %% pts/netrun_cli/01_helpers.pct.py 6 def write_config_data(data: dict, path: Path) -> None: """Write a raw config dict back to a file (.json or .toml). diff --git a/netrun/src/netrun/cli/_recipes.py b/netrun-cli/src/netrun_cli/_recipes.py similarity index 88% rename from netrun/src/netrun/cli/_recipes.py rename to netrun-cli/src/netrun_cli/_recipes.py index e67e6c0e..0abc9507 100644 --- a/netrun/src/netrun/cli/_recipes.py +++ b/netrun-cli/src/netrun_cli/_recipes.py @@ -1,19 +1,19 @@ -# AUTOGENERATED! DO NOT EDIT! File to edit: pts/netrun/10_cli/04_recipes.pct.py +# AUTOGENERATED! DO NOT EDIT! File to edit: pts/netrun_cli/04_recipes.pct.py __all__ = ['recipes_app', 'recipes_list', 'recipes_run'] -# %% pts/netrun/10_cli/04_recipes.pct.py 2 +# %% pts/netrun_cli/04_recipes.pct.py 2 import json from pathlib import Path from typing import Annotated, Optional import typer -from ..cli._helpers import ConfigOpt, PrettyOpt, load_raw_data, output_json -from ..tools._helpers import get_recipes -from ..tools._recipes import execute_recipe, get_recipe_prompts +from ._helpers import ConfigOpt, PrettyOpt, load_raw_data, output_json +from netrun.tools._helpers import get_recipes +from netrun.tools._recipes import execute_recipe, get_recipe_prompts -# %% pts/netrun/10_cli/04_recipes.pct.py 4 +# %% pts/netrun_cli/04_recipes.pct.py 4 recipes_app = typer.Typer(help="List and run recipes.", no_args_is_help=True) diff --git a/netrun/src/tests/cli/test_cli.py b/netrun-cli/src/tests/test_cli.py similarity index 66% rename from netrun/src/tests/cli/test_cli.py rename to netrun-cli/src/tests/test_cli.py index 01fd2410..4202e01a 100644 --- a/netrun/src/tests/cli/test_cli.py +++ b/netrun-cli/src/tests/test_cli.py @@ -1,8 +1,8 @@ -# AUTOGENERATED! DO NOT EDIT! File to edit: pts/tests/10_cli/test_cli.pct.py +# AUTOGENERATED! DO NOT EDIT! File to edit: pts/tests/test_cli.pct.py -__all__ = ['BASIC_CONFIG', 'POOLS_CONFIG', 'SAMPLE_DIR', 'runner', 'test_actions_list_basic', 'test_actions_run_global_and_node_conflict', 'test_actions_run_global_not_found', 'test_actions_run_not_found', 'test_actions_run_requires_node_or_global', 'test_convert_bad_extension', 'test_convert_json_to_toml', 'test_convert_not_found', 'test_download_agents_branch_option', 'test_download_agents_network_error', 'test_download_agents_no_files', 'test_download_agents_partial_failure', 'test_download_agents_success', 'test_factory_info', 'test_factory_info_bad_module', 'test_help', 'test_info_basic', 'test_info_pools', 'test_node_detail', 'test_node_not_found', 'test_nodes_basic', 'test_recipes_list_empty', 'test_recipes_list_pools', 'test_recipes_run_not_found', 'test_structure_basic', 'test_structure_node_has_factory', 'test_validate_basic', 'test_validate_not_found', 'test_validate_pools', 'test_validate_pretty', 'test_validate_reports_resolution_failure'] +__all__ = ['BASIC_CONFIG', 'POOLS_CONFIG', 'SAMPLE_DIR', 'runner', 'test_actions_list_basic', 'test_actions_run_global_and_node_conflict', 'test_actions_run_global_not_found', 'test_actions_run_not_found', 'test_actions_run_requires_node_or_global', 'test_convert_bad_extension', 'test_convert_json_to_toml', 'test_convert_not_found', 'test_download_agents_branch_option', 'test_download_agents_network_error', 'test_download_agents_no_files', 'test_download_agents_partial_failure', 'test_download_agents_success', 'test_dry_run_basic', 'test_dry_run_depends_on_and_resources', 'test_dry_run_execution_order', 'test_dry_run_sink_nodes', 'test_factory_info', 'test_factory_info_bad_module', 'test_help', 'test_info_basic', 'test_info_pools', 'test_node_detail', 'test_node_edges_incoming', 'test_node_edges_none', 'test_node_edges_outgoing', 'test_node_no_edges_by_default', 'test_node_not_found', 'test_nodes_basic', 'test_recipes_list_empty', 'test_recipes_list_pools', 'test_recipes_run_not_found', 'test_structure_bad_format', 'test_structure_basic', 'test_structure_json_default', 'test_structure_mermaid', 'test_structure_node_has_factory', 'test_validate_basic', 'test_validate_not_found', 'test_validate_pools', 'test_validate_pretty', 'test_validate_reports_resolution_failure'] -# %% pts/tests/10_cli/test_cli.pct.py 2 +# %% pts/tests/test_cli.pct.py 2 import json import os from pathlib import Path @@ -10,16 +10,16 @@ import pytest from typer.testing import CliRunner -from netrun.cli._app import app +from netrun_cli._app import app -# %% pts/tests/10_cli/test_cli.pct.py 3 +# %% pts/tests/test_cli.pct.py 3 runner = CliRunner() -SAMPLE_DIR = Path(__file__).resolve().parent.parent.parent.parent.parent / "sample_projects" +SAMPLE_DIR = Path(__file__).resolve().parent.parent.parent.parent / "sample_projects" BASIC_CONFIG = str(SAMPLE_DIR / "00_basic_net_project" / "main.netrun.json") POOLS_CONFIG = str(SAMPLE_DIR / "01_thread_and_process_pools" / "main.netrun.json") -# %% pts/tests/10_cli/test_cli.pct.py 5 +# %% pts/tests/test_cli.pct.py 5 def test_validate_basic(): result = runner.invoke(app, ["validate", "-c", BASIC_CONFIG]) assert result.exit_code == 0 @@ -48,7 +48,7 @@ def test_validate_pretty(): data = json.loads(result.stdout) assert data["valid"] is True -# %% pts/tests/10_cli/test_cli.pct.py 7 +# %% pts/tests/test_cli.pct.py 7 def test_structure_basic(): result = runner.invoke(app, ["structure", "-c", BASIC_CONFIG]) assert result.exit_code == 0 @@ -68,7 +68,7 @@ def test_structure_node_has_factory(): assert node["factory"] == "netrun.node_factories.from_function" assert node["factory_args"]["func"] == "nodes.double" -# %% pts/tests/10_cli/test_cli.pct.py 9 +# %% pts/tests/test_cli.pct.py 9 def test_convert_json_to_toml(): result = runner.invoke(app, ["convert", BASIC_CONFIG]) assert result.exit_code == 0 @@ -85,7 +85,7 @@ def test_convert_bad_extension(): result = runner.invoke(app, ["convert", "/some/file.txt"]) assert result.exit_code == 1 -# %% pts/tests/10_cli/test_cli.pct.py 11 +# %% pts/tests/test_cli.pct.py 11 def test_factory_info(): result = runner.invoke(app, ["factory-info", "netrun.node_factories.from_function", "--pretty"]) assert result.exit_code == 0 @@ -102,7 +102,7 @@ def test_factory_info_bad_module(): result = runner.invoke(app, ["factory-info", "nonexistent.module"]) assert result.exit_code == 1 -# %% pts/tests/10_cli/test_cli.pct.py 13 +# %% pts/tests/test_cli.pct.py 13 def test_info_basic(): result = runner.invoke(app, ["info", "-c", BASIC_CONFIG, "--pretty"]) assert result.exit_code == 0 @@ -122,7 +122,7 @@ def test_info_pools(): assert data["pools"]["processes"] == "multiprocess" assert data["recipes"] == 1 -# %% pts/tests/10_cli/test_cli.pct.py 15 +# %% pts/tests/test_cli.pct.py 15 def test_nodes_basic(): result = runner.invoke(app, ["nodes", "-c", BASIC_CONFIG]) assert result.exit_code == 0 @@ -134,7 +134,7 @@ def test_nodes_basic(): assert "format_result" in names assert "analyze" in names -# %% pts/tests/10_cli/test_cli.pct.py 17 +# %% pts/tests/test_cli.pct.py 17 def test_node_detail(): result = runner.invoke(app, ["node", "double", "-c", BASIC_CONFIG, "--pretty"]) assert result.exit_code == 0 @@ -148,7 +148,7 @@ def test_node_not_found(): result = runner.invoke(app, ["node", "nonexistent", "-c", BASIC_CONFIG]) assert result.exit_code == 1 -# %% pts/tests/10_cli/test_cli.pct.py 19 +# %% pts/tests/test_cli.pct.py 19 def test_actions_list_basic(): result = runner.invoke(app, ["actions", "list", "-c", BASIC_CONFIG]) assert result.exit_code == 0 @@ -178,7 +178,7 @@ def test_actions_run_global_and_node_conflict(): result = runner.invoke(app, ["actions", "run", "some_action", "double", "--global", "-c", BASIC_CONFIG]) assert result.exit_code == 2 -# %% pts/tests/10_cli/test_cli.pct.py 21 +# %% pts/tests/test_cli.pct.py 21 def test_recipes_list_empty(): result = runner.invoke(app, ["recipes", "list", "-c", BASIC_CONFIG]) assert result.exit_code == 0 @@ -198,7 +198,7 @@ def test_recipes_run_not_found(): result = runner.invoke(app, ["recipes", "run", "nonexistent", "-c", BASIC_CONFIG]) assert result.exit_code == 1 -# %% pts/tests/10_cli/test_cli.pct.py 23 +# %% pts/tests/test_cli.pct.py 23 from unittest.mock import patch, MagicMock import urllib.error @@ -249,7 +249,7 @@ def test_download_agents_success(tmp_path): mock_fn = _make_mock_urlopen(tree, file_contents) out_dir = str(tmp_path / "agents") - with patch("netrun.cli._download_agents.urllib.request.urlopen", side_effect=mock_fn): + with patch("netrun_cli._download_agents.urllib.request.urlopen", side_effect=mock_fn): result = runner.invoke(app, ["download-agents", out_dir]) assert result.exit_code == 0 @@ -266,7 +266,7 @@ def test_download_agents_no_files(tmp_path): mock_fn = _make_mock_urlopen(tree) out_dir = str(tmp_path / "agents") - with patch("netrun.cli._download_agents.urllib.request.urlopen", side_effect=mock_fn): + with patch("netrun_cli._download_agents.urllib.request.urlopen", side_effect=mock_fn): result = runner.invoke(app, ["download-agents", out_dir]) assert result.exit_code == 1 @@ -277,7 +277,7 @@ def test_download_agents_network_error(tmp_path): out_dir = str(tmp_path / "agents") with patch( - "netrun.cli._download_agents.urllib.request.urlopen", + "netrun_cli._download_agents.urllib.request.urlopen", side_effect=urllib.error.URLError("connection refused"), ): result = runner.invoke(app, ["download-agents", out_dir]) @@ -291,7 +291,7 @@ def test_download_agents_branch_option(tmp_path): mock_fn = _make_mock_urlopen(tree, {"agents/INSTRUCTIONS.md": b"instructions"}) out_dir = str(tmp_path / "agents") - with patch("netrun.cli._download_agents.urllib.request.urlopen", side_effect=mock_fn) as _: + with patch("netrun_cli._download_agents.urllib.request.urlopen", side_effect=mock_fn) as _: result = runner.invoke(app, ["download-agents", out_dir, "-b", "dev"]) assert result.exit_code == 0 @@ -331,7 +331,7 @@ def mock_urlopen(req_or_url, **kwargs): out_dir = str(tmp_path / "agents") - with patch("netrun.cli._download_agents.urllib.request.urlopen", side_effect=mock_urlopen): + with patch("netrun_cli._download_agents.urllib.request.urlopen", side_effect=mock_urlopen): result = runner.invoke(app, ["download-agents", out_dir]) assert result.exit_code == 0 @@ -339,7 +339,7 @@ def mock_urlopen(req_or_url, **kwargs): assert "FAILED" in result.output assert (tmp_path / "agents" / "good.md").read_bytes() == b"good content" -# %% pts/tests/10_cli/test_cli.pct.py 25 +# %% pts/tests/test_cli.pct.py 25 def test_help(): result = runner.invoke(app, ["--help"]) assert result.exit_code == 0 @@ -353,8 +353,9 @@ def test_help(): assert "actions" in result.stdout assert "recipes" in result.stdout assert "download-agents" in result.stdout + assert "dry-run" in result.stdout -# %% pts/tests/10_cli/test_cli.pct.py 27 +# %% pts/tests/test_cli.pct.py 27 import tempfile def test_validate_reports_resolution_failure(): @@ -397,3 +398,144 @@ def test_validate_reports_resolution_failure(): "resolution" in str(m).lower() or "module" in str(m).lower() for m in all_messages ), f"validate silently swallowed resolution failure: {data}" + +# %% pts/tests/test_cli.pct.py 29 +def test_node_edges_incoming(): + result = runner.invoke(app, ["node", "add", "--edges", "-c", BASIC_CONFIG, "--pretty"]) + assert result.exit_code == 0 + data = json.loads(result.stdout) + assert "edges" in data + assert "incoming" in data["edges"] + assert "outgoing" in data["edges"] + # "add" has an incoming edge from "double" + incoming = data["edges"]["incoming"] + assert len(incoming) >= 1 + assert any(e["source"] == "double.out" and e["port"] == "a" for e in incoming) + + +def test_node_edges_outgoing(): + result = runner.invoke(app, ["node", "double", "--edges", "-c", BASIC_CONFIG, "--pretty"]) + assert result.exit_code == 0 + data = json.loads(result.stdout) + outgoing = data["edges"]["outgoing"] + assert len(outgoing) == 1 + assert outgoing[0]["port"] == "out" + assert outgoing[0]["target"] == "add.a" + + +def test_node_edges_none(): + """Node with no connected edges returns empty lists.""" + result = runner.invoke(app, ["node", "analyze", "--edges", "-c", BASIC_CONFIG, "--pretty"]) + assert result.exit_code == 0 + data = json.loads(result.stdout) + assert data["edges"]["incoming"] == [] + assert data["edges"]["outgoing"] == [] + + +def test_node_no_edges_by_default(): + result = runner.invoke(app, ["node", "add", "-c", BASIC_CONFIG, "--pretty"]) + assert result.exit_code == 0 + data = json.loads(result.stdout) + assert "edges" not in data + +# %% pts/tests/test_cli.pct.py 31 +def test_structure_mermaid(): + result = runner.invoke(app, ["structure", "-c", BASIC_CONFIG, "--format", "mermaid"]) + assert result.exit_code == 0 + lines = result.stdout.strip().split("\n") + assert lines[0] == "graph LR" + # Node declarations + assert any('double["double"]' in line for line in lines) + assert any('add["add"]' in line for line in lines) + # Edge with label + assert any('double -->|"out → a"| add' in line for line in lines) + + +def test_structure_json_default(): + """Existing JSON output still works without --format.""" + result = runner.invoke(app, ["structure", "-c", BASIC_CONFIG]) + assert result.exit_code == 0 + data = json.loads(result.stdout) + assert "nodes" in data + assert "edges" in data + + +def test_structure_bad_format(): + result = runner.invoke(app, ["structure", "-c", BASIC_CONFIG, "--format", "xml"]) + assert result.exit_code == 1 + +# %% pts/tests/test_cli.pct.py 33 +def test_dry_run_basic(): + result = runner.invoke(app, ["dry-run", "-c", BASIC_CONFIG, "--pretty"]) + assert result.exit_code == 0 + data = json.loads(result.stdout) + assert "source_nodes" in data + assert "sink_nodes" in data + assert "execution_order" in data + assert data["total_nodes"] == 4 + assert data["total_edges"] == 2 + # double and analyze have no incoming edges → source nodes + assert "double" in data["source_nodes"] + assert "analyze" in data["source_nodes"] + + +def test_dry_run_execution_order(): + result = runner.invoke(app, ["dry-run", "-c", BASIC_CONFIG]) + assert result.exit_code == 0 + data = json.loads(result.stdout) + order = [e["node"] for e in data["execution_order"]] + # double must come before add (double→add edge), add before format_result + assert order.index("double") < order.index("add") + assert order.index("add") < order.index("format_result") + + +def test_dry_run_sink_nodes(): + result = runner.invoke(app, ["dry-run", "-c", BASIC_CONFIG]) + assert result.exit_code == 0 + data = json.loads(result.stdout) + # format_result and analyze have no outgoing data edges + assert "format_result" in data["sink_nodes"] + assert "analyze" in data["sink_nodes"] + + +def test_dry_run_depends_on_and_resources(tmp_path): + """Test dry-run output includes depends_on and resources metadata.""" + config_data = { + "graph": { + "nodes": [ + { + "name": "A", + "in_ports": {"in": {}}, + "out_ports": {"out": {}}, + "execution_config": {"pools": ["main"]}, + }, + { + "name": "B", + "in_ports": {"in": {}}, + "out_ports": {"out": {}}, + "execution_config": { + "pools": ["main"], + "depends_on": ["A"], + "resources": {"gpu": 1}, + }, + }, + ], + "edges": [], + }, + "pools": {"main": {"spec": {"type": "main"}}}, + } + config_path = tmp_path / "test.netrun.json" + config_path.write_text(json.dumps(config_data)) + + result = runner.invoke(app, ["dry-run", "-c", str(config_path), "--pretty"]) + assert result.exit_code == 0 + data = json.loads(result.stdout) + + order = [e["node"] for e in data["execution_order"]] + # A must come before B because of depends_on + assert order.index("A") < order.index("B") + + # B should have depends_on and resources in its entry + b_entry = next(e for e in data["execution_order"] if e["node"] == "B") + assert b_entry["depends_on"] == ["A"] + assert b_entry["resources"] == {"gpu": 1} diff --git a/netrun/src/tests/cli/test_cli_graph.py b/netrun-cli/src/tests/test_cli_graph.py similarity index 95% rename from netrun/src/tests/cli/test_cli_graph.py rename to netrun-cli/src/tests/test_cli_graph.py index 91528f96..4527ea90 100644 --- a/netrun/src/tests/cli/test_cli_graph.py +++ b/netrun-cli/src/tests/test_cli_graph.py @@ -1,17 +1,17 @@ -# AUTOGENERATED! DO NOT EDIT! File to edit: pts/tests/10_cli/test_cli_graph.pct.py +# AUTOGENERATED! DO NOT EDIT! File to edit: pts/tests/test_cli_graph.pct.py __all__ = ['runner', 'test_add_edge_basic', 'test_add_edge_dependency', 'test_add_edge_fan_out_warning', 'test_add_edge_missing_node', 'test_add_node_auto_position', 'test_add_node_custom_position', 'test_add_node_duplicate_error', 'test_add_node_factory', 'test_add_node_json_stdin', 'test_add_node_raw_ports', 'test_edit_node_merge', 'test_edit_node_not_found', 'test_edit_node_ports', 'test_edit_node_rename', 'test_remove_edge_basic', 'test_remove_edge_not_found', 'test_remove_node_not_found', 'test_remove_node_with_edges', 'test_validate_fan_out_suggestion'] -# %% pts/tests/10_cli/test_cli_graph.pct.py 2 +# %% pts/tests/test_cli_graph.pct.py 2 import json from pathlib import Path import pytest from typer.testing import CliRunner -from netrun.cli._app import app +from netrun_cli._app import app -# %% pts/tests/10_cli/test_cli_graph.pct.py 3 +# %% pts/tests/test_cli_graph.pct.py 3 runner = CliRunner() @@ -46,7 +46,7 @@ def _base_config() -> dict: } } -# %% pts/tests/10_cli/test_cli_graph.pct.py 5 +# %% pts/tests/test_cli_graph.pct.py 5 def test_add_node_factory(tmp_path): cfg = _make_config(tmp_path, _base_config()) result = runner.invoke(app, [ @@ -123,7 +123,7 @@ def test_add_node_auto_position(tmp_path): assert node["extra"]["ui"]["position"]["x"] == 700.0 assert node["extra"]["ui"]["position"]["y"] == 200.0 -# %% pts/tests/10_cli/test_cli_graph.pct.py 7 +# %% pts/tests/test_cli_graph.pct.py 7 def test_remove_node_with_edges(tmp_path): cfg = _make_config(tmp_path, _base_config()) result = runner.invoke(app, ["remove-node", "A", "-c", cfg, "--no-validate"]) @@ -144,7 +144,7 @@ def test_remove_node_not_found(tmp_path): assert result.exit_code == 1 assert "not found" in result.output -# %% pts/tests/10_cli/test_cli_graph.pct.py 9 +# %% pts/tests/test_cli_graph.pct.py 9 def test_edit_node_rename(tmp_path): cfg = _make_config(tmp_path, _base_config()) result = runner.invoke(app, ["edit-node", "A", "-c", cfg, "--rename", "Alpha", "--no-validate"]) @@ -195,7 +195,7 @@ def test_edit_node_not_found(tmp_path): assert result.exit_code == 1 assert "not found" in result.output -# %% pts/tests/10_cli/test_cli_graph.pct.py 11 +# %% pts/tests/test_cli_graph.pct.py 11 def test_add_edge_basic(tmp_path): data = _base_config() data["graph"]["edges"] = [] # Start with no edges @@ -238,7 +238,7 @@ def test_add_edge_missing_node(tmp_path): assert result.exit_code == 1 assert "not found" in result.output -# %% pts/tests/10_cli/test_cli_graph.pct.py 13 +# %% pts/tests/test_cli_graph.pct.py 13 def test_remove_edge_basic(tmp_path): cfg = _make_config(tmp_path, _base_config()) result = runner.invoke(app, ["remove-edge", "A", "out", "B", "in", "-c", cfg, "--no-validate"]) @@ -257,7 +257,7 @@ def test_remove_edge_not_found(tmp_path): assert result.exit_code == 1 assert "not found" in result.output -# %% pts/tests/10_cli/test_cli_graph.pct.py 15 +# %% pts/tests/test_cli_graph.pct.py 15 def test_validate_fan_out_suggestion(): from netrun.net.config._graph import GraphConfig from netrun.net.config._nodes import NodeConfig, PortConfig, EdgeConfig diff --git a/netrun-cli/uv.lock b/netrun-cli/uv.lock new file mode 100644 index 00000000..17208ccc --- /dev/null +++ b/netrun-cli/uv.lock @@ -0,0 +1,1323 @@ +version = 1 +revision = 3 +requires-python = ">=3.11" + +[options] +exclude-newer = "2026-04-21T10:25:15.039084Z" +exclude-newer-span = "P7D" + +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "attrs" +version = "26.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, +] + +[[package]] +name = "beartype" +version = "0.22.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/94/1009e248bbfbab11397abca7193bea6626806be9a327d399810d523a07cb/beartype-0.22.9.tar.gz", hash = "sha256:8f82b54aa723a2848a56008d18875f91c1db02c32ef6a62319a002e3e25a975f", size = 1608866, upload-time = "2025-12-13T06:50:30.72Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/cc/18245721fa7747065ab478316c7fea7c74777d07f37ae60db2e84f8172e8/beartype-0.22.9-py3-none-any.whl", hash = "sha256:d16c9bbc61ea14637596c5f6fbff2ee99cbe3573e46a716401734ef50c3060c2", size = 1333658, upload-time = "2025-12-13T06:50:28.266Z" }, +] + +[[package]] +name = "beautifulsoup4" +version = "4.14.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "soupsieve" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c3/b0/1c6a16426d389813b48d95e26898aff79abbde42ad353958ad95cc8c9b21/beautifulsoup4-4.14.3.tar.gz", hash = "sha256:6292b1c5186d356bba669ef9f7f051757099565ad9ada5dd630bd9de5fa7fb86", size = 627737, upload-time = "2025-11-30T15:08:26.084Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-py3-none-any.whl", hash = "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb", size = 107721, upload-time = "2025-11-30T15:08:24.087Z" }, +] + +[[package]] +name = "bleach" +version = "6.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "webencodings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/07/18/3c8523962314be6bf4c8989c79ad9531c825210dd13a8669f6b84336e8bd/bleach-6.3.0.tar.gz", hash = "sha256:6f3b91b1c0a02bb9a78b5a454c92506aa0fdf197e1d5e114d2e00c6f64306d22", size = 203533, upload-time = "2025-10-27T17:57:39.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cd/3a/577b549de0cc09d95f11087ee63c739bba856cd3952697eec4c4bb91350a/bleach-6.3.0-py3-none-any.whl", hash = "sha256:fe10ec77c93ddf3d13a73b035abaac7a9f5e436513864ccdad516693213c65d6", size = 164437, upload-time = "2025-10-27T17:57:37.538Z" }, +] + +[package.optional-dependencies] +css = [ + { name = "tinycss2" }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, + { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, + { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, + { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, + { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, + { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, + { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, + { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, + { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, +] + +[[package]] +name = "click" +version = "8.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/57/75/31212c6bf2503fdf920d87fee5d7a86a2e3bcf444984126f13d8e4016804/click-8.3.2.tar.gz", hash = "sha256:14162b8b3b3550a7d479eafa77dfd3c38d9dc8951f6f69c78913a8f9a7540fd5", size = 302856, upload-time = "2026-04-03T19:14:45.118Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/20/71885d8b97d4f3dde17b1fdb92dbd4908b00541c5a3379787137285f602e/click-8.3.2-py3-none-any.whl", hash = "sha256:1924d2c27c5653561cd2cae4548d1406039cb79b858b747cfea24924bbc1616d", size = 108379, upload-time = "2026-04-03T19:14:43.505Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "defusedxml" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520, upload-time = "2021-03-08T10:59:26.269Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" }, +] + +[[package]] +name = "diskcache" +version = "5.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3f/21/1c1ffc1a039ddcc459db43cc108658f32c57d271d7289a2794e401d0fdb6/diskcache-5.6.3.tar.gz", hash = "sha256:2c3a3fa2743d8535d832ec61c2054a1641f41775aa7c556758a109941e33e4fc", size = 67916, upload-time = "2023-08-31T06:12:00.316Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/27/4570e78fc0bf5ea0ca45eb1de3818a23787af9b390c0b0a0033a1b8236f9/diskcache-5.6.3-py3-none-any.whl", hash = "sha256:5e31b2d5fbad117cc363ebaf6b689474db18a1f6438bc82358b024abd4c2ca19", size = 45550, upload-time = "2023-08-31T06:11:58.822Z" }, +] + +[[package]] +name = "fastjsonschema" +version = "2.21.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/b5/23b216d9d985a956623b6bd12d4086b60f0059b27799f23016af04a74ea1/fastjsonschema-2.21.2.tar.gz", hash = "sha256:b1eb43748041c880796cd077f1a07c3d94e93ae84bba5ed36800a33554ae05de", size = 374130, upload-time = "2025-08-14T18:49:36.666Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/a8/20d0723294217e47de6d9e2e40fd4a9d2f7c4b6ef974babd482a59743694/fastjsonschema-2.21.2-py3-none-any.whl", hash = "sha256:1c797122d0a86c5cace2e54bf4e819c36223b552017172f32c5c024a6b77e463", size = 24024, upload-time = "2025-08-14T18:49:34.776Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "jsonschema" +version = "4.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, +] + +[[package]] +name = "jupyter-client" +version = "8.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jupyter-core" }, + { name = "python-dateutil" }, + { name = "pyzmq" }, + { name = "tornado" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/05/e4/ba649102a3bc3fbca54e7239fb924fd434c766f855693d86de0b1f2bec81/jupyter_client-8.8.0.tar.gz", hash = "sha256:d556811419a4f2d96c869af34e854e3f059b7cc2d6d01a9cd9c85c267691be3e", size = 348020, upload-time = "2026-01-08T13:55:47.938Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/0b/ceb7694d864abc0a047649aec263878acb9f792e1fec3e676f22dc9015e3/jupyter_client-8.8.0-py3-none-any.whl", hash = "sha256:f93a5b99c5e23a507b773d3a1136bd6e16c67883ccdbd9a829b0bbdb98cd7d7a", size = 107371, upload-time = "2026-01-08T13:55:45.562Z" }, +] + +[[package]] +name = "jupyter-core" +version = "5.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "platformdirs" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/49/9d1284d0dc65e2c757b74c6687b6d319b02f822ad039e5c512df9194d9dd/jupyter_core-5.9.1.tar.gz", hash = "sha256:4d09aaff303b9566c3ce657f580bd089ff5c91f5f89cf7d8846c3cdf465b5508", size = 89814, upload-time = "2025-10-16T19:19:18.444Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl", hash = "sha256:ebf87fdc6073d142e114c72c9e29a9d7ca03fad818c5d300ce2adc1fb0743407", size = 29032, upload-time = "2025-10-16T19:19:16.783Z" }, +] + +[[package]] +name = "jupyterlab-pygments" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/90/51/9187be60d989df97f5f0aba133fa54e7300f17616e065d1ada7d7646b6d6/jupyterlab_pygments-0.3.0.tar.gz", hash = "sha256:721aca4d9029252b11cfa9d185e5b5af4d54772bb8072f9b7036f4170054d35d", size = 512900, upload-time = "2023-11-23T09:26:37.44Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/dd/ead9d8ea85bf202d90cc513b533f9c363121c7792674f78e0d8a854b63b4/jupyterlab_pygments-0.3.0-py3-none-any.whl", hash = "sha256:841a89020971da1d8693f1a99997aefc5dc424bb1b251fd6322462a1b8842780", size = 15884, upload-time = "2023-11-23T09:26:34.325Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, + { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, + { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, + { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, + { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, + { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "mistune" +version = "3.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/55/d01f0c4b45ade6536c51170b9043db8b2ec6ddf4a35c7ea3f5f559ac935b/mistune-3.2.0.tar.gz", hash = "sha256:708487c8a8cdd99c9d90eb3ed4c3ed961246ff78ac82f03418f5183ab70e398a", size = 95467, upload-time = "2025-12-23T11:36:34.994Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/f7/4a5e785ec9fbd65146a27b6b70b6cdc161a66f2024e4b04ac06a67f5578b/mistune-3.2.0-py3-none-any.whl", hash = "sha256:febdc629a3c78616b94393c6580551e0e34cc289987ec6c35ed3f4be42d0eee1", size = 53598, upload-time = "2025-12-23T11:36:33.211Z" }, +] + +[[package]] +name = "nbclient" +version = "0.10.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jupyter-client" }, + { name = "jupyter-core" }, + { name = "nbformat" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/56/91/1c1d5a4b9a9ebba2b4e32b8c852c2975c872aec1fe42ab5e516b2cecd193/nbclient-0.10.4.tar.gz", hash = "sha256:1e54091b16e6da39e297b0ece3e10f6f29f4ac4e8ee515d29f8a7099bd6553c9", size = 62554, upload-time = "2025-12-23T07:45:46.369Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/a0/5b0c2f11142ed1dddec842457d3f65eaf71a0080894eb6f018755b319c3a/nbclient-0.10.4-py3-none-any.whl", hash = "sha256:9162df5a7373d70d606527300a95a975a47c137776cd942e52d9c7e29ff83440", size = 25465, upload-time = "2025-12-23T07:45:44.51Z" }, +] + +[[package]] +name = "nbconvert" +version = "7.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beautifulsoup4" }, + { name = "bleach", extra = ["css"] }, + { name = "defusedxml" }, + { name = "jinja2" }, + { name = "jupyter-core" }, + { name = "jupyterlab-pygments" }, + { name = "markupsafe" }, + { name = "mistune" }, + { name = "nbclient" }, + { name = "nbformat" }, + { name = "packaging" }, + { name = "pandocfilters" }, + { name = "pygments" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/01/b1/708e53fe2e429c103c6e6e159106bcf0357ac41aa4c28772bd8402339051/nbconvert-7.17.1.tar.gz", hash = "sha256:34d0d0a7e73ce3cbab6c5aae8f4f468797280b01fd8bd2ca746da8569eddd7d2", size = 865311, upload-time = "2026-04-08T00:44:14.914Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/f8/bb0a9d5f46819c821dc1f004aa2cc29b1d91453297dbf5ff20470f00f193/nbconvert-7.17.1-py3-none-any.whl", hash = "sha256:aa85c087b435e7bf1ffd03319f658e285f2b89eccab33bc1ba7025495ab3e7c8", size = 261927, upload-time = "2026-04-08T00:44:12.845Z" }, +] + +[[package]] +name = "nbformat" +version = "5.10.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fastjsonschema" }, + { name = "jsonschema" }, + { name = "jupyter-core" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/fd/91545e604bc3dad7dca9ed03284086039b294c6b3d75c0d2fa45f9e9caf3/nbformat-5.10.4.tar.gz", hash = "sha256:322168b14f937a5d11362988ecac2a4952d3d8e3a2cbeb2319584631226d5b3a", size = 142749, upload-time = "2024-04-04T11:20:37.371Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/82/0340caa499416c78e5d8f5f05947ae4bc3cba53c9f038ab6e9ed964e22f1/nbformat-5.10.4-py3-none-any.whl", hash = "sha256:3b48d6c8fbca4b299bf3982ea7db1af21580e4fec269ad087b9e81588891200b", size = 78454, upload-time = "2024-04-04T11:20:34.895Z" }, +] + +[[package]] +name = "nblite" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jinja2" }, + { name = "nbconvert" }, + { name = "nbformat" }, + { name = "notebookx-py" }, + { name = "pydantic" }, + { name = "pyyaml" }, + { name = "rich" }, + { name = "typer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/80/a5/e7a91b57e1dc17d0e546e95500388b957320588a563c20c053eb593b3353/nblite-1.2.1.tar.gz", hash = "sha256:0f5b129d93069f0f7502ddcc1f8d960af78eefb9d097b1b85bad088ec1618cbb", size = 325768, upload-time = "2026-03-15T10:50:07.001Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/4d/471d79351bab69bf96f12d3b689d430342857138bb45fb82cb08a19125d1/nblite-1.2.1-py3-none-any.whl", hash = "sha256:04083f525229f8d8445c08dc192286491ec63e730806c84faf226b9d33b35ca0", size = 105264, upload-time = "2026-03-15T10:50:05.235Z" }, +] + +[[package]] +name = "netrun" +version = "0.5.0" +source = { editable = "../netrun" } +dependencies = [ + { name = "beartype" }, + { name = "diskcache" }, + { name = "nblite" }, + { name = "netrun-sim" }, + { name = "websockets" }, + { name = "xxhash" }, +] + +[package.metadata] +requires-dist = [ + { name = "beartype", specifier = ">=0.22.9" }, + { name = "cloudpickle", marker = "extra == 'serialization'", specifier = ">=3.0" }, + { name = "dill", marker = "extra == 'serialization'", specifier = ">=0.3" }, + { name = "diskcache", specifier = ">=5.6.3" }, + { name = "nblite", specifier = ">=1.1.1" }, + { name = "netrun-sim", editable = "../netrun-sim/python" }, + { name = "pyinfra", marker = "extra == 'deploy'", specifier = ">=3.0" }, + { name = "websockets", specifier = ">=16.0" }, + { name = "xxhash", specifier = ">=3.6.0" }, +] +provides-extras = ["deploy", "serialization"] + +[package.metadata.requires-dev] +dev = [ + { name = "dev", editable = "../netrun/src/dev" }, + { name = "jupyterlab", specifier = ">=4.5.1" }, + { name = "mypy", specifier = ">=1.19.1" }, + { name = "nblite", specifier = ">=1.1.5" }, + { name = "netrun-cli", editable = "." }, + { name = "pytest", specifier = ">=9.0.2" }, + { name = "pytest-asyncio", specifier = ">=1.3.0" }, + { name = "ruff", specifier = ">=0.8.0" }, +] + +[[package]] +name = "netrun-cli" +version = "0.5.0" +source = { editable = "." } +dependencies = [ + { name = "netrun" }, + { name = "tomli-w" }, + { name = "typer" }, +] + +[package.dev-dependencies] +dev = [ + { name = "nblite" }, + { name = "pytest" }, + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [ + { name = "netrun", editable = "../netrun" }, + { name = "tomli-w", specifier = ">=1.0" }, + { name = "typer", specifier = ">=0.15.0" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "nblite", specifier = ">=1.1.5" }, + { name = "pytest", specifier = ">=9.0.2" }, + { name = "ruff", specifier = ">=0.8.0" }, +] + +[[package]] +name = "netrun-sim" +version = "0.2.0" +source = { editable = "../netrun-sim/python" } +dependencies = [ + { name = "python-ulid" }, +] + +[package.metadata] +requires-dist = [{ name = "python-ulid", specifier = ">=1.0" }] + +[package.metadata.requires-dev] +dev = [ + { name = "git-cliff", specifier = ">=2.11.0" }, + { name = "jupyterlab", specifier = ">=4.3.8" }, + { name = "maturin", specifier = ">=1.10.2" }, + { name = "nblite", specifier = ">=1.1.1" }, + { name = "python-ulid", specifier = ">=1.1.0" }, + { name = "ruff", specifier = ">=0.14.11" }, + { name = "twine", specifier = ">=6.1.0" }, + { name = "typing-extensions", specifier = ">=4.13.2" }, +] + +[[package]] +name = "notebookx-py" +version = "0.1.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/8f/a052edfbddf51d4ec485bf05ec0b526fc9d4d476cbdea01b60a040b4dd4c/notebookx_py-0.1.8.tar.gz", hash = "sha256:cee26716a9436b7ee0771ec840c49d268068d1e7688b9a4d3da79c763cd07a11", size = 40139, upload-time = "2026-01-22T19:31:59.907Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/24/f1d34cf2dca8ad9c008345d6900460991535e9b708979a39e6c6df8580a6/notebookx_py-0.1.8-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:e30a0677f752acaa47bbee12f00b6a3a9caf48e1b10637699dc956c99dbcf17e", size = 762017, upload-time = "2026-01-22T19:31:48.983Z" }, + { url = "https://files.pythonhosted.org/packages/54/78/a43c065515b8a26a36e1b1ce96f15b166e4c5beb4d026317adc6db86074f/notebookx_py-0.1.8-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:6adfd281c4c4c4a6dea7ae9e9c5ed4e04359a1425a78d9e33acdd3914c579752", size = 732635, upload-time = "2026-01-22T19:31:51.027Z" }, + { url = "https://files.pythonhosted.org/packages/89/ed/7141c4baf3ef1d0ee80bd18b0c6c5b1ee45d6320c86c5c850fd2ab1d8d35/notebookx_py-0.1.8-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ae180b4ef7939b4567ca97e60c23acac79e5c17d66ccbca51f1b4faa72ed35a", size = 782241, upload-time = "2026-01-22T19:31:52.379Z" }, + { url = "https://files.pythonhosted.org/packages/ba/f3/b97946c846113f86537e202679d3322e06f5014d86e06fe061767d1cafd2/notebookx_py-0.1.8-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d34febdababaec0b5fca2355d3dbd6f69c3b0b596b26daec5ffa66c754bda94", size = 806947, upload-time = "2026-01-22T19:31:54.329Z" }, + { url = "https://files.pythonhosted.org/packages/07/0d/266611c951bea05711a2bf4791b035720a9daec1181d46589acc57d9f38c/notebookx_py-0.1.8-cp38-abi3-win_amd64.whl", hash = "sha256:fa4d013e4272d3ae714d5ca2206db9d263f85064ecb7380ee7bf722dee5d4625", size = 624603, upload-time = "2026-01-22T19:31:55.687Z" }, +] + +[[package]] +name = "packaging" +version = "26.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/df/de/0d2b39fb4af88a0258f3bac87dfcbb48e73fbdea4a2ed0e2213f9a4c2f9a/packaging-26.1.tar.gz", hash = "sha256:f042152b681c4bfac5cae2742a55e103d27ab2ec0f3d88037136b6bfe7c9c5de", size = 215519, upload-time = "2026-04-14T21:12:49.362Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/c2/920ef838e2f0028c8262f16101ec09ebd5969864e5a64c4c05fad0617c56/packaging-26.1-py3-none-any.whl", hash = "sha256:5d9c0669c6285e491e0ced2eee587eaf67b670d94a19e94e3984a481aba6802f", size = 95831, upload-time = "2026-04-14T21:12:47.56Z" }, +] + +[[package]] +name = "pandocfilters" +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/70/6f/3dd4940bbe001c06a65f88e36bad298bc7a0de5036115639926b0c5c0458/pandocfilters-1.5.1.tar.gz", hash = "sha256:002b4a555ee4ebc03f8b66307e287fa492e4a77b4ea14d3f934328297bb4939e", size = 8454, upload-time = "2024-01-18T20:08:13.726Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/af/4fbc8cab944db5d21b7e2a5b8e9211a03a79852b1157e2c102fcc61ac440/pandocfilters-1.5.1-py2.py3-none-any.whl", hash = "sha256:93be382804a9cdb0a7267585f157e5d1731bbe5545a85b268d6f5fe6232de2bc", size = 8663, upload-time = "2024-01-18T20:08:11.28Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.9.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9f/4a/0883b8e3802965322523f0b200ecf33d31f10991d0401162f4b23c698b42/platformdirs-4.9.6.tar.gz", hash = "sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a", size = 29400, upload-time = "2026-04-09T00:04:10.812Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/a6/a0a304dc33b49145b21f4808d763822111e67d1c3a32b524a1baf947b6e1/platformdirs-4.9.6-py3-none-any.whl", hash = "sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917", size = 21348, upload-time = "2026-04-09T00:04:09.463Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + +[[package]] +name = "pydantic" +version = "2.13.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d9/e4/40d09941a2cebcb20609b86a559817d5b9291c49dd6f8c87e5feffbe703a/pydantic-2.13.3.tar.gz", hash = "sha256:af09e9d1d09f4e7fe37145c1f577e1d61ceb9a41924bf0094a36506285d0a84d", size = 844068, upload-time = "2026-04-20T14:46:43.632Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/0a/fd7d723f8f8153418fb40cf9c940e82004fce7e987026b08a68a36dd3fe7/pydantic-2.13.3-py3-none-any.whl", hash = "sha256:6db14ac8dfc9a1e57f87ea2c0de670c251240f43cb0c30a5130e9720dc612927", size = 471981, upload-time = "2026-04-20T14:46:41.402Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.46.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2a/ef/f7abb56c49382a246fd2ce9c799691e3c3e7175ec74b14d99e798bcddb1a/pydantic_core-2.46.3.tar.gz", hash = "sha256:41c178f65b8c29807239d47e6050262eb6bf84eb695e41101e62e38df4a5bc2c", size = 471412, upload-time = "2026-04-20T14:40:56.672Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/a2/1ba90a83e85a3f94c796b184f3efde9c72f2830dcda493eea8d59ba78e6d/pydantic_core-2.46.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ab124d49d0459b2373ecf54118a45c28a1e6d4192a533fbc915e70f556feb8e5", size = 2106740, upload-time = "2026-04-20T14:41:20.932Z" }, + { url = "https://files.pythonhosted.org/packages/b6/f6/99ae893c89a0b9d3daec9f95487aa676709aa83f67643b3f0abaf4ab628a/pydantic_core-2.46.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cca67d52a5c7a16aed2b3999e719c4bcf644074eac304a5d3d62dd70ae7d4b2c", size = 1948293, upload-time = "2026-04-20T14:43:42.115Z" }, + { url = "https://files.pythonhosted.org/packages/3e/b8/2e8e636dc9e3f16c2e16bf0849e24be82c5ee82c603c65fc0326666328fc/pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c024e08c0ba23e6fd68c771a521e9d6a792f2ebb0fa734296b36394dc30390e", size = 1973222, upload-time = "2026-04-20T14:41:57.841Z" }, + { url = "https://files.pythonhosted.org/packages/34/36/0e730beec4d83c5306f417afbd82ff237d9a21e83c5edf675f31ed84c1fe/pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6645ce7eec4928e29a1e3b3d5c946621d105d3e79f0c9cddf07c2a9770949287", size = 2053852, upload-time = "2026-04-20T14:40:43.077Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f0/3071131f47e39136a17814576e0fada9168569f7f8c0e6ac4d1ede6a4958/pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a712c7118e6c5ea96562f7b488435172abb94a3c53c22c9efc1412264a45cbbe", size = 2221134, upload-time = "2026-04-20T14:43:03.349Z" }, + { url = "https://files.pythonhosted.org/packages/2f/a9/a2dc023eec5aa4b02a467874bad32e2446957d2adcab14e107eab502e978/pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:69a868ef3ff206343579021c40faf3b1edc64b1cc508ff243a28b0a514ccb050", size = 2279785, upload-time = "2026-04-20T14:41:19.285Z" }, + { url = "https://files.pythonhosted.org/packages/0a/44/93f489d16fb63fbd41c670441536541f6e8cfa1e5a69f40bc9c5d30d8c90/pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc7e8c32db809aa0f6ea1d6869ebc8518a65d5150fdfad8bcae6a49ae32a22e2", size = 2089404, upload-time = "2026-04-20T14:43:10.108Z" }, + { url = "https://files.pythonhosted.org/packages/2a/78/8692e3aa72b2d004f7a5d937f1dfdc8552ba26caf0bec75f342c40f00dec/pydantic_core-2.46.3-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:3481bd1341dc85779ee506bc8e1196a277ace359d89d28588a9468c3ecbe63fa", size = 2114898, upload-time = "2026-04-20T14:44:51.475Z" }, + { url = "https://files.pythonhosted.org/packages/6a/62/e83133f2e7832532060175cebf1f13748f4c7e7e7165cdd1f611f174494b/pydantic_core-2.46.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8690eba565c6d68ffd3a8655525cbdd5246510b44a637ee2c6c03a7ebfe64d3c", size = 2157856, upload-time = "2026-04-20T14:43:46.64Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ec/6a500e3ad7718ee50583fae79c8651f5d37e3abce1fa9ae177ae65842c53/pydantic_core-2.46.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4de88889d7e88d50d40ee5b39d5dac0bcaef9ba91f7e536ac064e6b2834ecccf", size = 2180168, upload-time = "2026-04-20T14:42:00.302Z" }, + { url = "https://files.pythonhosted.org/packages/d8/53/8267811054b1aa7fc1dc7ded93812372ef79a839f5e23558136a6afbfde1/pydantic_core-2.46.3-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:e480080975c1ef7f780b8f99ed72337e7cc5efea2e518a20a692e8e7b278eb8b", size = 2322885, upload-time = "2026-04-20T14:41:05.253Z" }, + { url = "https://files.pythonhosted.org/packages/c8/c1/1c0acdb3aa0856ddc4ecc55214578f896f2de16f400cf51627eb3c26c1c4/pydantic_core-2.46.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:de3a5c376f8cd94da9a1b8fd3dd1c16c7a7b216ed31dc8ce9fd7a22bf13b836e", size = 2360328, upload-time = "2026-04-20T14:41:43.991Z" }, + { url = "https://files.pythonhosted.org/packages/f0/d0/ef39cd0f4a926814f360e71c1adeab48ad214d9727e4deb48eedfb5bce1a/pydantic_core-2.46.3-cp311-cp311-win32.whl", hash = "sha256:fc331a5314ffddd5385b9ee9d0d2fee0b13c27e0e02dad71b1ae5d6561f51eeb", size = 1979464, upload-time = "2026-04-20T14:43:12.215Z" }, + { url = "https://files.pythonhosted.org/packages/18/9c/f41951b0d858e343f1cf09398b2a7b3014013799744f2c4a8ad6a3eec4f2/pydantic_core-2.46.3-cp311-cp311-win_amd64.whl", hash = "sha256:b5b9c6cf08a8a5e502698f5e153056d12c34b8fb30317e0c5fd06f45162a6346", size = 2070837, upload-time = "2026-04-20T14:41:47.707Z" }, + { url = "https://files.pythonhosted.org/packages/9f/1e/264a17cd582f6ed50950d4d03dd5fefd84e570e238afe1cb3e25cf238769/pydantic_core-2.46.3-cp311-cp311-win_arm64.whl", hash = "sha256:5dfd51cf457482f04ec49491811a2b8fd5b843b64b11eecd2d7a1ee596ea78a6", size = 2053647, upload-time = "2026-04-20T14:42:27.535Z" }, + { url = "https://files.pythonhosted.org/packages/4b/cb/5b47425556ecc1f3fe18ed2a0083188aa46e1dd812b06e406475b3a5d536/pydantic_core-2.46.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b11b59b3eee90a80a36701ddb4576d9ae31f93f05cb9e277ceaa09e6bf074a67", size = 2101946, upload-time = "2026-04-20T14:40:52.581Z" }, + { url = "https://files.pythonhosted.org/packages/a1/4f/2fb62c2267cae99b815bbf4a7b9283812c88ca3153ef29f7707200f1d4e5/pydantic_core-2.46.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:af8653713055ea18a3abc1537fe2ebc42f5b0bbb768d1eb79fd74eb47c0ac089", size = 1951612, upload-time = "2026-04-20T14:42:42.996Z" }, + { url = "https://files.pythonhosted.org/packages/50/6e/b7348fd30d6556d132cddd5bd79f37f96f2601fe0608afac4f5fb01ec0b3/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:75a519dab6d63c514f3a81053e5266c549679e4aa88f6ec57f2b7b854aceb1b0", size = 1977027, upload-time = "2026-04-20T14:42:02.001Z" }, + { url = "https://files.pythonhosted.org/packages/82/11/31d60ee2b45540d3fb0b29302a393dbc01cd771c473f5b5147bcd353e593/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a6cd87cb1575b1ad05ba98894c5b5c96411ef678fa2f6ed2576607095b8d9789", size = 2063008, upload-time = "2026-04-20T14:44:17.952Z" }, + { url = "https://files.pythonhosted.org/packages/8a/db/3a9d1957181b59258f44a2300ab0f0be9d1e12d662a4f57bb31250455c52/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f80a55484b8d843c8ada81ebf70a682f3f00a3d40e378c06cf17ecb44d280d7d", size = 2233082, upload-time = "2026-04-20T14:40:57.934Z" }, + { url = "https://files.pythonhosted.org/packages/9c/e1/3277c38792aeb5cfb18c2f0c5785a221d9ff4e149abbe1184d53d5f72273/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3861f1731b90c50a3266316b9044f5c9b405eecb8e299b0a7120596334e4fe9c", size = 2304615, upload-time = "2026-04-20T14:42:12.584Z" }, + { url = "https://files.pythonhosted.org/packages/5e/d5/e3d9717c9eba10855325650afd2a9cba8e607321697f18953af9d562da2f/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb528e295ed31570ac3dcc9bfdd6e0150bc11ce6168ac87a8082055cf1a67395", size = 2094380, upload-time = "2026-04-20T14:43:05.522Z" }, + { url = "https://files.pythonhosted.org/packages/a1/20/abac35dedcbfd66c6f0b03e4e3564511771d6c9b7ede10a362d03e110d9b/pydantic_core-2.46.3-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:367508faa4973b992b271ba1494acaab36eb7e8739d1e47be5035fb1ea225396", size = 2135429, upload-time = "2026-04-20T14:41:55.549Z" }, + { url = "https://files.pythonhosted.org/packages/6c/a5/41bfd1df69afad71b5cf0535055bccc73022715ad362edbc124bc1e021d7/pydantic_core-2.46.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5ad3c826fe523e4becf4fe39baa44286cff85ef137c729a2c5e269afbfd0905d", size = 2174582, upload-time = "2026-04-20T14:41:45.96Z" }, + { url = "https://files.pythonhosted.org/packages/79/65/38d86ea056b29b2b10734eb23329b7a7672ca604df4f2b6e9c02d4ee22fe/pydantic_core-2.46.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ec638c5d194ef8af27db69f16c954a09797c0dc25015ad6123eb2c73a4d271ca", size = 2187533, upload-time = "2026-04-20T14:40:55.367Z" }, + { url = "https://files.pythonhosted.org/packages/b6/55/a1129141678a2026badc539ad1dee0a71d06f54c2f06a4bd68c030ac781b/pydantic_core-2.46.3-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:28ed528c45446062ee66edb1d33df5d88828ae167de76e773a3c7f64bd14e976", size = 2332985, upload-time = "2026-04-20T14:44:13.05Z" }, + { url = "https://files.pythonhosted.org/packages/d7/60/cb26f4077719f709e54819f4e8e1d43f4091f94e285eb6bd21e1190a7b7c/pydantic_core-2.46.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aed19d0c783886d5bd86d80ae5030006b45e28464218747dcf83dabfdd092c7b", size = 2373670, upload-time = "2026-04-20T14:41:53.421Z" }, + { url = "https://files.pythonhosted.org/packages/6b/7e/c3f21882bdf1d8d086876f81b5e296206c69c6082551d776895de7801fa0/pydantic_core-2.46.3-cp312-cp312-win32.whl", hash = "sha256:06d5d8820cbbdb4147578c1fe7ffcd5b83f34508cb9f9ab76e807be7db6ff0a4", size = 1966722, upload-time = "2026-04-20T14:44:30.588Z" }, + { url = "https://files.pythonhosted.org/packages/57/be/6b5e757b859013ebfbd7adba02f23b428f37c86dcbf78b5bb0b4ffd36e99/pydantic_core-2.46.3-cp312-cp312-win_amd64.whl", hash = "sha256:c3212fda0ee959c1dd04c60b601ec31097aaa893573a3a1abd0a47bcac2968c1", size = 2072970, upload-time = "2026-04-20T14:42:54.248Z" }, + { url = "https://files.pythonhosted.org/packages/bf/f8/a989b21cc75e9a32d24192ef700eea606521221a89faa40c919ce884f2b1/pydantic_core-2.46.3-cp312-cp312-win_arm64.whl", hash = "sha256:f1f8338dd7a7f31761f1f1a3c47503a9a3b34eea3c8b01fa6ee96408affb5e72", size = 2035963, upload-time = "2026-04-20T14:44:20.4Z" }, + { url = "https://files.pythonhosted.org/packages/9b/3c/9b5e8eb9821936d065439c3b0fb1490ffa64163bfe7e1595985a47896073/pydantic_core-2.46.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:12bc98de041458b80c86c56b24df1d23832f3e166cbaff011f25d187f5c62c37", size = 2102109, upload-time = "2026-04-20T14:41:24.219Z" }, + { url = "https://files.pythonhosted.org/packages/91/97/1c41d1f5a19f241d8069f1e249853bcce378cdb76eec8ab636d7bc426280/pydantic_core-2.46.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:85348b8f89d2c3508b65b16c3c33a4da22b8215138d8b996912bb1532868885f", size = 1951820, upload-time = "2026-04-20T14:42:14.236Z" }, + { url = "https://files.pythonhosted.org/packages/30/b4/d03a7ae14571bc2b6b3c7b122441154720619afe9a336fa3a95434df5e2f/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1105677a6df914b1fb71a81b96c8cce7726857e1717d86001f29be06a25ee6f8", size = 1977785, upload-time = "2026-04-20T14:42:31.648Z" }, + { url = "https://files.pythonhosted.org/packages/ae/0c/4086f808834b59e3c8f1aa26df8f4b6d998cdcf354a143d18ef41529d1fe/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:87082cd65669a33adeba5470769e9704c7cf026cc30afb9cc77fd865578ebaad", size = 2062761, upload-time = "2026-04-20T14:40:37.093Z" }, + { url = "https://files.pythonhosted.org/packages/fa/71/a649be5a5064c2df0db06e0a512c2281134ed2fcc981f52a657936a7527c/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60e5f66e12c4f5212d08522963380eaaeac5ebd795826cfd19b2dfb0c7a52b9c", size = 2232989, upload-time = "2026-04-20T14:42:59.254Z" }, + { url = "https://files.pythonhosted.org/packages/a2/84/7756e75763e810b3a710f4724441d1ecc5883b94aacb07ca71c5fb5cfb69/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b6cdf19bf84128d5e7c37e8a73a0c5c10d51103a650ac585d42dd6ae233f2b7f", size = 2303975, upload-time = "2026-04-20T14:41:32.287Z" }, + { url = "https://files.pythonhosted.org/packages/6c/35/68a762e0c1e31f35fa0dac733cbd9f5b118042853698de9509c8e5bf128b/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:031bb17f4885a43773c8c763089499f242aee2ea85cf17154168775dccdecf35", size = 2095325, upload-time = "2026-04-20T14:42:47.685Z" }, + { url = "https://files.pythonhosted.org/packages/77/bf/1bf8c9a8e91836c926eae5e3e51dce009bf495a60ca56060689d3df3f340/pydantic_core-2.46.3-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:bcf2a8b2982a6673693eae7348ef3d8cf3979c1d63b54fca7c397a635cc68687", size = 2133368, upload-time = "2026-04-20T14:41:22.766Z" }, + { url = "https://files.pythonhosted.org/packages/e5/50/87d818d6bab915984995157ceb2380f5aac4e563dddbed6b56f0ed057aba/pydantic_core-2.46.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28e8cf2f52d72ced402a137145923a762cbb5081e48b34312f7a0c8f55928ec3", size = 2173908, upload-time = "2026-04-20T14:42:52.044Z" }, + { url = "https://files.pythonhosted.org/packages/91/88/a311fb306d0bd6185db41fa14ae888fb81d0baf648a761ae760d30819d33/pydantic_core-2.46.3-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:17eaface65d9fc5abb940003020309c1bf7a211f5f608d7870297c367e6f9022", size = 2186422, upload-time = "2026-04-20T14:43:29.55Z" }, + { url = "https://files.pythonhosted.org/packages/8f/79/28fd0d81508525ab2054fef7c77a638c8b5b0afcbbaeee493cf7c3fef7e1/pydantic_core-2.46.3-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:93fd339f23408a07e98950a89644f92c54d8729719a40b30c0a30bb9ebc55d23", size = 2332709, upload-time = "2026-04-20T14:42:16.134Z" }, + { url = "https://files.pythonhosted.org/packages/b3/21/795bf5fe5c0f379308b8ef19c50dedab2e7711dbc8d0c2acf08f1c7daa05/pydantic_core-2.46.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:23cbdb3aaa74dfe0837975dbf69b469753bbde8eacace524519ffdb6b6e89eb7", size = 2372428, upload-time = "2026-04-20T14:41:10.974Z" }, + { url = "https://files.pythonhosted.org/packages/45/b3/ed14c659cbe7605e3ef063077680a64680aec81eb1a04763a05190d49b7f/pydantic_core-2.46.3-cp313-cp313-win32.whl", hash = "sha256:610eda2e3838f401105e6326ca304f5da1e15393ae25dacae5c5c63f2c275b13", size = 1965601, upload-time = "2026-04-20T14:41:42.128Z" }, + { url = "https://files.pythonhosted.org/packages/ef/bb/adb70d9a762ddd002d723fbf1bd492244d37da41e3af7b74ad212609027e/pydantic_core-2.46.3-cp313-cp313-win_amd64.whl", hash = "sha256:68cc7866ed863db34351294187f9b729964c371ba33e31c26f478471c52e1ed0", size = 2071517, upload-time = "2026-04-20T14:43:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/52/eb/66faefabebfe68bd7788339c9c9127231e680b11906368c67ce112fdb47f/pydantic_core-2.46.3-cp313-cp313-win_arm64.whl", hash = "sha256:f64b5537ac62b231572879cd08ec05600308636a5d63bcbdb15063a466977bec", size = 2035802, upload-time = "2026-04-20T14:43:38.507Z" }, + { url = "https://files.pythonhosted.org/packages/7f/db/a7bcb4940183fda36022cd18ba8dd12f2dff40740ec7b58ce7457befa416/pydantic_core-2.46.3-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:afa3aa644f74e290cdede48a7b0bee37d1c35e71b05105f6b340d484af536d9b", size = 2097614, upload-time = "2026-04-20T14:44:38.374Z" }, + { url = "https://files.pythonhosted.org/packages/24/35/e4066358a22e3e99519db370494c7528f5a2aa1367370e80e27e20283543/pydantic_core-2.46.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ced3310e51aa425f7f77da8bbbb5212616655bedbe82c70944320bc1dbe5e018", size = 1951896, upload-time = "2026-04-20T14:40:53.996Z" }, + { url = "https://files.pythonhosted.org/packages/87/92/37cf4049d1636996e4b888c05a501f40a43ff218983a551d57f9d5e14f0d/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e29908922ce9da1a30b4da490bd1d3d82c01dcfdf864d2a74aacee674d0bfa34", size = 1979314, upload-time = "2026-04-20T14:41:49.446Z" }, + { url = "https://files.pythonhosted.org/packages/d8/36/9ff4d676dfbdfb2d591cf43f3d90ded01e15b1404fd101180ed2d62a2fd3/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0c9ff69140423eea8ed2d5477df3ba037f671f5e897d206d921bc9fdc39613e7", size = 2056133, upload-time = "2026-04-20T14:42:23.574Z" }, + { url = "https://files.pythonhosted.org/packages/bc/f0/405b442a4d7ba855b06eec8b2bf9c617d43b8432d099dfdc7bf999293495/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b675ab0a0d5b1c8fdb81195dc5bcefea3f3c240871cdd7ff9a2de8aa50772eb2", size = 2228726, upload-time = "2026-04-20T14:44:22.816Z" }, + { url = "https://files.pythonhosted.org/packages/e7/f8/65cd92dd5a0bd89ba277a98ecbfaf6fc36bbd3300973c7a4b826d6ab1391/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0087084960f209a9a4af50ecd1fb063d9ad3658c07bb81a7a53f452dacbfb2ba", size = 2301214, upload-time = "2026-04-20T14:44:48.792Z" }, + { url = "https://files.pythonhosted.org/packages/fd/86/ef96a4c6e79e7a2d0410826a68fbc0eccc0fd44aa733be199d5fcac3bb87/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed42e6cc8e1b0e2b9b96e2276bad70ae625d10d6d524aed0c93de974ae029f9f", size = 2099927, upload-time = "2026-04-20T14:41:40.196Z" }, + { url = "https://files.pythonhosted.org/packages/6d/53/269caf30e0096e0a8a8f929d1982a27b3879872cca2d917d17c2f9fdf4fe/pydantic_core-2.46.3-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:f1771ce258afb3e4201e67d154edbbae712a76a6081079fe247c2f53c6322c22", size = 2128789, upload-time = "2026-04-20T14:41:15.868Z" }, + { url = "https://files.pythonhosted.org/packages/00/b0/1a6d9b6a587e118482910c244a1c5acf4d192604174132efd12bf0ac486f/pydantic_core-2.46.3-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a7610b6a5242a6c736d8ad47fd5fff87fcfe8f833b281b1c409c3d6835d9227f", size = 2173815, upload-time = "2026-04-20T14:44:25.152Z" }, + { url = "https://files.pythonhosted.org/packages/87/56/e7e00d4041a7e62b5a40815590114db3b535bf3ca0bf4dca9f16cef25246/pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:ff5e7783bcc5476e1db448bf268f11cb257b1c276d3e89f00b5727be86dd0127", size = 2181608, upload-time = "2026-04-20T14:41:28.933Z" }, + { url = "https://files.pythonhosted.org/packages/e8/22/4bd23c3d41f7c185d60808a1de83c76cf5aeabf792f6c636a55c3b1ec7f9/pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:9d2e32edcc143bc01e95300671915d9ca052d4f745aa0a49c48d4803f8a85f2c", size = 2326968, upload-time = "2026-04-20T14:42:03.962Z" }, + { url = "https://files.pythonhosted.org/packages/24/ac/66cd45129e3915e5ade3b292cb3bc7fd537f58f8f8dbdaba6170f7cabb74/pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:6e42d83d1c6b87fa56b521479cff237e626a292f3b31b6345c15a99121b454c1", size = 2369842, upload-time = "2026-04-20T14:41:35.52Z" }, + { url = "https://files.pythonhosted.org/packages/a2/51/dd4248abb84113615473aa20d5545b7c4cd73c8644003b5259686f93996c/pydantic_core-2.46.3-cp314-cp314-win32.whl", hash = "sha256:07bc6d2a28c3adb4f7c6ae46aa4f2d2929af127f587ed44057af50bf1ce0f505", size = 1959661, upload-time = "2026-04-20T14:41:00.042Z" }, + { url = "https://files.pythonhosted.org/packages/20/eb/59980e5f1ae54a3b86372bd9f0fa373ea2d402e8cdcd3459334430f91e91/pydantic_core-2.46.3-cp314-cp314-win_amd64.whl", hash = "sha256:8940562319bc621da30714617e6a7eaa6b98c84e8c685bcdc02d7ed5e7c7c44e", size = 2071686, upload-time = "2026-04-20T14:43:16.471Z" }, + { url = "https://files.pythonhosted.org/packages/8c/db/1cf77e5247047dfee34bc01fa9bca134854f528c8eb053e144298893d370/pydantic_core-2.46.3-cp314-cp314-win_arm64.whl", hash = "sha256:5dcbbcf4d22210ced8f837c96db941bdb078f419543472aca5d9a0bb7cddc7df", size = 2026907, upload-time = "2026-04-20T14:43:31.732Z" }, + { url = "https://files.pythonhosted.org/packages/57/c0/b3df9f6a543276eadba0a48487b082ca1f201745329d97dbfa287034a230/pydantic_core-2.46.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:d0fe3dce1e836e418f912c1ad91c73357d03e556a4d286f441bf34fed2dbeecf", size = 2095047, upload-time = "2026-04-20T14:42:37.982Z" }, + { url = "https://files.pythonhosted.org/packages/66/57/886a938073b97556c168fd99e1a7305bb363cd30a6d2c76086bf0587b32a/pydantic_core-2.46.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9ce92e58abc722dac1bf835a6798a60b294e48eb0e625ec9fd994b932ac5feee", size = 1934329, upload-time = "2026-04-20T14:43:49.655Z" }, + { url = "https://files.pythonhosted.org/packages/0b/7c/b42eaa5c34b13b07ecb51da21761297a9b8eb43044c864a035999998f328/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a03e6467f0f5ab796a486146d1b887b2dc5e5f9b3288898c1b1c3ad974e53e4a", size = 1974847, upload-time = "2026-04-20T14:42:10.737Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9b/92b42db6543e7de4f99ae977101a2967b63122d4b6cf7773812da2d7d5b5/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2798b6ba041b9d70acfb9071a2ea13c8456dd1e6a5555798e41ba7b0790e329c", size = 2041742, upload-time = "2026-04-20T14:40:44.262Z" }, + { url = "https://files.pythonhosted.org/packages/0f/19/46fbe1efabb5aa2834b43b9454e70f9a83ad9c338c1291e48bdc4fecf167/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9be3e221bdc6d69abf294dcf7aff6af19c31a5cdcc8f0aa3b14be29df4bd03b1", size = 2236235, upload-time = "2026-04-20T14:41:27.307Z" }, + { url = "https://files.pythonhosted.org/packages/77/da/b3f95bc009ad60ec53120f5d16c6faa8cabdbe8a20d83849a1f2b8728148/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f13936129ce841f2a5ddf6f126fea3c43cd128807b5a59588c37cf10178c2e64", size = 2282633, upload-time = "2026-04-20T14:44:33.271Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6e/401336117722e28f32fb8220df676769d28ebdf08f2f4469646d404c43a3/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28b5f2ef03416facccb1c6ef744c69793175fd27e44ef15669201601cf423acb", size = 2109679, upload-time = "2026-04-20T14:44:41.065Z" }, + { url = "https://files.pythonhosted.org/packages/fc/53/b289f9bc8756a32fe718c46f55afaeaf8d489ee18d1a1e7be1db73f42cc4/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:830d1247d77ad23852314f069e9d7ddafeec5f684baf9d7e7065ed46a049c4e6", size = 2108342, upload-time = "2026-04-20T14:42:50.144Z" }, + { url = "https://files.pythonhosted.org/packages/10/5b/8292fc7c1f9111f1b2b7c1b0dcf1179edcd014fc3ea4517499f50b829d71/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0793c90c1a3c74966e7975eaef3ed30ebdff3260a0f815a62a22adc17e4c01c", size = 2157208, upload-time = "2026-04-20T14:42:08.133Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9e/f80044e9ec07580f057a89fc131f78dda7a58751ddf52bbe05eaf31db50f/pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:d2d0aead851b66f5245ec0c4fb2612ef457f8bbafefdf65a2bf9d6bac6140f47", size = 2167237, upload-time = "2026-04-20T14:42:25.412Z" }, + { url = "https://files.pythonhosted.org/packages/f8/84/6781a1b037f3b96be9227edbd1101f6d3946746056231bf4ac48cdff1a8d/pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:2f40e4246676beb31c5ce77c38a55ca4e465c6b38d11ea1bd935420568e0b1ab", size = 2312540, upload-time = "2026-04-20T14:40:40.313Z" }, + { url = "https://files.pythonhosted.org/packages/3e/db/19c0839feeb728e7df03255581f198dfdf1c2aeb1e174a8420b63c5252e5/pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:cf489cf8986c543939aeee17a09c04d6ffb43bfef8ca16fcbcc5cfdcbed24dba", size = 2369556, upload-time = "2026-04-20T14:41:09.427Z" }, + { url = "https://files.pythonhosted.org/packages/e0/15/3228774cb7cd45f5f721ddf1b2242747f4eb834d0c491f0c02d606f09fed/pydantic_core-2.46.3-cp314-cp314t-win32.whl", hash = "sha256:ffe0883b56cfc05798bf994164d2b2ff03efe2d22022a2bb080f3b626176dd56", size = 1949756, upload-time = "2026-04-20T14:41:25.717Z" }, + { url = "https://files.pythonhosted.org/packages/b8/2a/c79cf53fd91e5a87e30d481809f52f9a60dd221e39de66455cf04deaad37/pydantic_core-2.46.3-cp314-cp314t-win_amd64.whl", hash = "sha256:706d9d0ce9cf4593d07270d8e9f53b161f90c57d315aeec4fb4fd7a8b10240d8", size = 2051305, upload-time = "2026-04-20T14:43:18.627Z" }, + { url = "https://files.pythonhosted.org/packages/0b/db/d8182a7f1d9343a032265aae186eb063fe26ca4c40f256b21e8da4498e89/pydantic_core-2.46.3-cp314-cp314t-win_arm64.whl", hash = "sha256:77706aeb41df6a76568434701e0917da10692da28cb69d5fb6919ce5fdb07374", size = 2026310, upload-time = "2026-04-20T14:41:01.778Z" }, + { url = "https://files.pythonhosted.org/packages/66/7f/03dbad45cd3aa9083fbc93c210ae8b005af67e4136a14186950a747c6874/pydantic_core-2.46.3-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:9715525891ed524a0a1eb6d053c74d4d4ad5017677fb00af0b7c2644a31bae46", size = 2105683, upload-time = "2026-04-20T14:42:19.779Z" }, + { url = "https://files.pythonhosted.org/packages/26/22/4dc186ac8ea6b257e9855031f51b62a9637beac4d68ac06bee02f046f836/pydantic_core-2.46.3-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:9d2f400712a99a013aff420ef1eb9be077f8189a36c1e3ef87660b4e1088a874", size = 1940052, upload-time = "2026-04-20T14:43:59.274Z" }, + { url = "https://files.pythonhosted.org/packages/0d/ca/d376391a5aff1f2e8188960d7873543608130a870961c2b6b5236627c116/pydantic_core-2.46.3-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd2aab0e2e9dc2daf36bd2686c982535d5e7b1d930a1344a7bb6e82baab42a76", size = 1988172, upload-time = "2026-04-20T14:41:17.469Z" }, + { url = "https://files.pythonhosted.org/packages/0e/6b/523b9f85c23788755d6ab949329de692a2e3a584bc6beb67fef5e035aa9d/pydantic_core-2.46.3-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e9d76736da5f362fabfeea6a69b13b7f2be405c6d6966f06b2f6bfff7e64531", size = 2128596, upload-time = "2026-04-20T14:40:41.707Z" }, + { url = "https://files.pythonhosted.org/packages/34/42/f426db557e8ab2791bc7562052299944a118655496fbff99914e564c0a94/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:b12dd51f1187c2eb489af8e20f880362db98e954b54ab792fa5d92e8bcc6b803", size = 2091877, upload-time = "2026-04-20T14:43:27.091Z" }, + { url = "https://files.pythonhosted.org/packages/5c/4f/86a832a9d14df58e663bfdf4627dc00d3317c2bd583c4fb23390b0f04b8e/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:f00a0961b125f1a47af7bcc17f00782e12f4cd056f83416006b30111d941dfa3", size = 1932428, upload-time = "2026-04-20T14:40:45.781Z" }, + { url = "https://files.pythonhosted.org/packages/11/1a/fe857968954d93fb78e0d4b6df5c988c74c4aaa67181c60be7cfe327c0ca/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57697d7c056aca4bbb680200f96563e841a6386ac1129370a0102592f4dddff5", size = 1997550, upload-time = "2026-04-20T14:44:02.425Z" }, + { url = "https://files.pythonhosted.org/packages/17/eb/9d89ad2d9b0ba8cd65393d434471621b98912abb10fbe1df08e480ba57b5/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd35aa21299def8db7ef4fe5c4ff862941a9a158ca7b63d61e66fe67d30416b4", size = 2137657, upload-time = "2026-04-20T14:42:45.149Z" }, + { url = "https://files.pythonhosted.org/packages/1f/da/99d40830684f81dec901cac521b5b91c095394cc1084b9433393cde1c2df/pydantic_core-2.46.3-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:13afdd885f3d71280cf286b13b310ee0f7ccfefd1dbbb661514a474b726e2f25", size = 2107973, upload-time = "2026-04-20T14:42:06.175Z" }, + { url = "https://files.pythonhosted.org/packages/99/a5/87024121818d75bbb2a98ddbaf638e40e7a18b5e0f5492c9ca4b1b316107/pydantic_core-2.46.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f91c0aff3e3ee0928edd1232c57f643a7a003e6edf1860bc3afcdc749cb513f3", size = 1947191, upload-time = "2026-04-20T14:43:14.319Z" }, + { url = "https://files.pythonhosted.org/packages/60/62/0c1acfe10945b83a6a59d19fbaa92f48825381509e5701b855c08f13db76/pydantic_core-2.46.3-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6529d1d128321a58d30afcc97b49e98836542f68dd41b33c2e972bb9e5290536", size = 2123791, upload-time = "2026-04-20T14:43:22.766Z" }, + { url = "https://files.pythonhosted.org/packages/75/3e/3b2393b4c8f44285561dc30b00cf307a56a2eff7c483a824db3b8221ca51/pydantic_core-2.46.3-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:975c267cff4f7e7272eacbe50f6cc03ca9a3da4c4fbd66fffd89c94c1e311aa1", size = 2153197, upload-time = "2026-04-20T14:44:27.932Z" }, + { url = "https://files.pythonhosted.org/packages/ba/75/5af02fb35505051eee727c061f2881c555ab4f8ddb2d42da715a42c9731b/pydantic_core-2.46.3-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:2b8e4f2bbdf71415c544b4b1138b8060db7b6611bc927e8064c769f64bed651c", size = 2181073, upload-time = "2026-04-20T14:43:20.729Z" }, + { url = "https://files.pythonhosted.org/packages/10/92/7e0e1bd9ca3c68305db037560ca2876f89b2647deb2f8b6319005de37505/pydantic_core-2.46.3-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:e61ea8e9fff9606d09178f577ff8ccdd7206ff73d6552bcec18e1033c4254b85", size = 2315886, upload-time = "2026-04-20T14:44:04.826Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d8/101655f27eaf3e44558ead736b2795d12500598beed4683f279396fa186e/pydantic_core-2.46.3-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b504bda01bafc69b6d3c7a0c7f039dcf60f47fab70e06fe23f57b5c75bdc82b8", size = 2360528, upload-time = "2026-04-20T14:40:47.431Z" }, + { url = "https://files.pythonhosted.org/packages/07/0f/1c34a74c8d07136f0d729ffe5e1fdab04fbdaa7684f61a92f92511a84a15/pydantic_core-2.46.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:b00b76f7142fc60c762ce579bd29c8fa44aaa56592dd3c54fab3928d0d4ca6ff", size = 2184144, upload-time = "2026-04-20T14:42:57Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, +] + +[[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, upload-time = "2024-03-01T18:36:20.211Z" } +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, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "python-ulid" +version = "3.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/40/7e/0d6c82b5ccc71e7c833aed43d9e8468e1f2ff0be1b3f657a6fcafbb8433d/python_ulid-3.1.0.tar.gz", hash = "sha256:ff0410a598bc5f6b01b602851a3296ede6f91389f913a5d5f8c496003836f636", size = 93175, upload-time = "2025-08-18T16:09:26.305Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6c/a0/4ed6632b70a52de845df056654162acdebaf97c20e3212c559ac43e7216e/python_ulid-3.1.0-py3-none-any.whl", hash = "sha256:e2cdc979c8c877029b4b7a38a6fba3bc4578e4f109a308419ff4d3ccf0a46619", size = 11577, upload-time = "2025-08-18T16:09:25.047Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "pyzmq" +version = "27.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "implementation_name == 'pypy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/0b/3c9baedbdf613ecaa7aa07027780b8867f57b6293b6ee50de316c9f3222b/pyzmq-27.1.0.tar.gz", hash = "sha256:ac0765e3d44455adb6ddbf4417dcce460fc40a05978c08efdf2948072f6db540", size = 281750, upload-time = "2025-09-08T23:10:18.157Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/5d/305323ba86b284e6fcb0d842d6adaa2999035f70f8c38a9b6d21ad28c3d4/pyzmq-27.1.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:226b091818d461a3bef763805e75685e478ac17e9008f49fce2d3e52b3d58b86", size = 1333328, upload-time = "2025-09-08T23:07:45.946Z" }, + { url = "https://files.pythonhosted.org/packages/bd/a0/fc7e78a23748ad5443ac3275943457e8452da67fda347e05260261108cbc/pyzmq-27.1.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:0790a0161c281ca9723f804871b4027f2e8b5a528d357c8952d08cd1a9c15581", size = 908803, upload-time = "2025-09-08T23:07:47.551Z" }, + { url = "https://files.pythonhosted.org/packages/7e/22/37d15eb05f3bdfa4abea6f6d96eb3bb58585fbd3e4e0ded4e743bc650c97/pyzmq-27.1.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c895a6f35476b0c3a54e3eb6ccf41bf3018de937016e6e18748317f25d4e925f", size = 668836, upload-time = "2025-09-08T23:07:49.436Z" }, + { url = "https://files.pythonhosted.org/packages/b1/c4/2a6fe5111a01005fc7af3878259ce17684fabb8852815eda6225620f3c59/pyzmq-27.1.0-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bbf8d3630bf96550b3be8e1fc0fea5cbdc8d5466c1192887bd94869da17a63e", size = 857038, upload-time = "2025-09-08T23:07:51.234Z" }, + { url = "https://files.pythonhosted.org/packages/cb/eb/bfdcb41d0db9cd233d6fb22dc131583774135505ada800ebf14dfb0a7c40/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:15c8bd0fe0dabf808e2d7a681398c4e5ded70a551ab47482067a572c054c8e2e", size = 1657531, upload-time = "2025-09-08T23:07:52.795Z" }, + { url = "https://files.pythonhosted.org/packages/ab/21/e3180ca269ed4a0de5c34417dfe71a8ae80421198be83ee619a8a485b0c7/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:bafcb3dd171b4ae9f19ee6380dfc71ce0390fefaf26b504c0e5f628d7c8c54f2", size = 2034786, upload-time = "2025-09-08T23:07:55.047Z" }, + { url = "https://files.pythonhosted.org/packages/3b/b1/5e21d0b517434b7f33588ff76c177c5a167858cc38ef740608898cd329f2/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e829529fcaa09937189178115c49c504e69289abd39967cd8a4c215761373394", size = 1894220, upload-time = "2025-09-08T23:07:57.172Z" }, + { url = "https://files.pythonhosted.org/packages/03/f2/44913a6ff6941905efc24a1acf3d3cb6146b636c546c7406c38c49c403d4/pyzmq-27.1.0-cp311-cp311-win32.whl", hash = "sha256:6df079c47d5902af6db298ec92151db82ecb557af663098b92f2508c398bb54f", size = 567155, upload-time = "2025-09-08T23:07:59.05Z" }, + { url = "https://files.pythonhosted.org/packages/23/6d/d8d92a0eb270a925c9b4dd039c0b4dc10abc2fcbc48331788824ef113935/pyzmq-27.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:190cbf120fbc0fc4957b56866830def56628934a9d112aec0e2507aa6a032b97", size = 633428, upload-time = "2025-09-08T23:08:00.663Z" }, + { url = "https://files.pythonhosted.org/packages/ae/14/01afebc96c5abbbd713ecfc7469cfb1bc801c819a74ed5c9fad9a48801cb/pyzmq-27.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:eca6b47df11a132d1745eb3b5b5e557a7dae2c303277aa0e69c6ba91b8736e07", size = 559497, upload-time = "2025-09-08T23:08:02.15Z" }, + { url = "https://files.pythonhosted.org/packages/92/e7/038aab64a946d535901103da16b953c8c9cc9c961dadcbf3609ed6428d23/pyzmq-27.1.0-cp312-abi3-macosx_10_15_universal2.whl", hash = "sha256:452631b640340c928fa343801b0d07eb0c3789a5ffa843f6e1a9cee0ba4eb4fc", size = 1306279, upload-time = "2025-09-08T23:08:03.807Z" }, + { url = "https://files.pythonhosted.org/packages/e8/5e/c3c49fdd0f535ef45eefcc16934648e9e59dace4a37ee88fc53f6cd8e641/pyzmq-27.1.0-cp312-abi3-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1c179799b118e554b66da67d88ed66cd37a169f1f23b5d9f0a231b4e8d44a113", size = 895645, upload-time = "2025-09-08T23:08:05.301Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e5/b0b2504cb4e903a74dcf1ebae157f9e20ebb6ea76095f6cfffea28c42ecd/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3837439b7f99e60312f0c926a6ad437b067356dc2bc2ec96eb395fd0fe804233", size = 652574, upload-time = "2025-09-08T23:08:06.828Z" }, + { url = "https://files.pythonhosted.org/packages/f8/9b/c108cdb55560eaf253f0cbdb61b29971e9fb34d9c3499b0e96e4e60ed8a5/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43ad9a73e3da1fab5b0e7e13402f0b2fb934ae1c876c51d0afff0e7c052eca31", size = 840995, upload-time = "2025-09-08T23:08:08.396Z" }, + { url = "https://files.pythonhosted.org/packages/c2/bb/b79798ca177b9eb0825b4c9998c6af8cd2a7f15a6a1a4272c1d1a21d382f/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0de3028d69d4cdc475bfe47a6128eb38d8bc0e8f4d69646adfbcd840facbac28", size = 1642070, upload-time = "2025-09-08T23:08:09.989Z" }, + { url = "https://files.pythonhosted.org/packages/9c/80/2df2e7977c4ede24c79ae39dcef3899bfc5f34d1ca7a5b24f182c9b7a9ca/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_i686.whl", hash = "sha256:cf44a7763aea9298c0aa7dbf859f87ed7012de8bda0f3977b6fb1d96745df856", size = 2021121, upload-time = "2025-09-08T23:08:11.907Z" }, + { url = "https://files.pythonhosted.org/packages/46/bd/2d45ad24f5f5ae7e8d01525eb76786fa7557136555cac7d929880519e33a/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f30f395a9e6fbca195400ce833c731e7b64c3919aa481af4d88c3759e0cb7496", size = 1878550, upload-time = "2025-09-08T23:08:13.513Z" }, + { url = "https://files.pythonhosted.org/packages/e6/2f/104c0a3c778d7c2ab8190e9db4f62f0b6957b53c9d87db77c284b69f33ea/pyzmq-27.1.0-cp312-abi3-win32.whl", hash = "sha256:250e5436a4ba13885494412b3da5d518cd0d3a278a1ae640e113c073a5f88edd", size = 559184, upload-time = "2025-09-08T23:08:15.163Z" }, + { url = "https://files.pythonhosted.org/packages/fc/7f/a21b20d577e4100c6a41795842028235998a643b1ad406a6d4163ea8f53e/pyzmq-27.1.0-cp312-abi3-win_amd64.whl", hash = "sha256:9ce490cf1d2ca2ad84733aa1d69ce6855372cb5ce9223802450c9b2a7cba0ccf", size = 619480, upload-time = "2025-09-08T23:08:17.192Z" }, + { url = "https://files.pythonhosted.org/packages/78/c2/c012beae5f76b72f007a9e91ee9401cb88c51d0f83c6257a03e785c81cc2/pyzmq-27.1.0-cp312-abi3-win_arm64.whl", hash = "sha256:75a2f36223f0d535a0c919e23615fc85a1e23b71f40c7eb43d7b1dedb4d8f15f", size = 552993, upload-time = "2025-09-08T23:08:18.926Z" }, + { url = "https://files.pythonhosted.org/packages/60/cb/84a13459c51da6cec1b7b1dc1a47e6db6da50b77ad7fd9c145842750a011/pyzmq-27.1.0-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:93ad4b0855a664229559e45c8d23797ceac03183c7b6f5b4428152a6b06684a5", size = 1122436, upload-time = "2025-09-08T23:08:20.801Z" }, + { url = "https://files.pythonhosted.org/packages/dc/b6/94414759a69a26c3dd674570a81813c46a078767d931a6c70ad29fc585cb/pyzmq-27.1.0-cp313-cp313-android_24_x86_64.whl", hash = "sha256:fbb4f2400bfda24f12f009cba62ad5734148569ff4949b1b6ec3b519444342e6", size = 1156301, upload-time = "2025-09-08T23:08:22.47Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ad/15906493fd40c316377fd8a8f6b1f93104f97a752667763c9b9c1b71d42d/pyzmq-27.1.0-cp313-cp313t-macosx_10_15_universal2.whl", hash = "sha256:e343d067f7b151cfe4eb3bb796a7752c9d369eed007b91231e817071d2c2fec7", size = 1341197, upload-time = "2025-09-08T23:08:24.286Z" }, + { url = "https://files.pythonhosted.org/packages/14/1d/d343f3ce13db53a54cb8946594e567410b2125394dafcc0268d8dda027e0/pyzmq-27.1.0-cp313-cp313t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:08363b2011dec81c354d694bdecaef4770e0ae96b9afea70b3f47b973655cc05", size = 897275, upload-time = "2025-09-08T23:08:26.063Z" }, + { url = "https://files.pythonhosted.org/packages/69/2d/d83dd6d7ca929a2fc67d2c3005415cdf322af7751d773524809f9e585129/pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d54530c8c8b5b8ddb3318f481297441af102517602b569146185fa10b63f4fa9", size = 660469, upload-time = "2025-09-08T23:08:27.623Z" }, + { url = "https://files.pythonhosted.org/packages/3e/cd/9822a7af117f4bc0f1952dbe9ef8358eb50a24928efd5edf54210b850259/pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f3afa12c392f0a44a2414056d730eebc33ec0926aae92b5ad5cf26ebb6cc128", size = 847961, upload-time = "2025-09-08T23:08:29.672Z" }, + { url = "https://files.pythonhosted.org/packages/9a/12/f003e824a19ed73be15542f172fd0ec4ad0b60cf37436652c93b9df7c585/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c65047adafe573ff023b3187bb93faa583151627bc9c51fc4fb2c561ed689d39", size = 1650282, upload-time = "2025-09-08T23:08:31.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4a/e82d788ed58e9a23995cee70dbc20c9aded3d13a92d30d57ec2291f1e8a3/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:90e6e9441c946a8b0a667356f7078d96411391a3b8f80980315455574177ec97", size = 2024468, upload-time = "2025-09-08T23:08:33.543Z" }, + { url = "https://files.pythonhosted.org/packages/d9/94/2da0a60841f757481e402b34bf4c8bf57fa54a5466b965de791b1e6f747d/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:add071b2d25f84e8189aaf0882d39a285b42fa3853016ebab234a5e78c7a43db", size = 1885394, upload-time = "2025-09-08T23:08:35.51Z" }, + { url = "https://files.pythonhosted.org/packages/4f/6f/55c10e2e49ad52d080dc24e37adb215e5b0d64990b57598abc2e3f01725b/pyzmq-27.1.0-cp313-cp313t-win32.whl", hash = "sha256:7ccc0700cfdf7bd487bea8d850ec38f204478681ea02a582a8da8171b7f90a1c", size = 574964, upload-time = "2025-09-08T23:08:37.178Z" }, + { url = "https://files.pythonhosted.org/packages/87/4d/2534970ba63dd7c522d8ca80fb92777f362c0f321900667c615e2067cb29/pyzmq-27.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:8085a9fba668216b9b4323be338ee5437a235fe275b9d1610e422ccc279733e2", size = 641029, upload-time = "2025-09-08T23:08:40.595Z" }, + { url = "https://files.pythonhosted.org/packages/f6/fa/f8aea7a28b0641f31d40dea42d7ef003fded31e184ef47db696bc74cd610/pyzmq-27.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:6bb54ca21bcfe361e445256c15eedf083f153811c37be87e0514934d6913061e", size = 561541, upload-time = "2025-09-08T23:08:42.668Z" }, + { url = "https://files.pythonhosted.org/packages/87/45/19efbb3000956e82d0331bafca5d9ac19ea2857722fa2caacefb6042f39d/pyzmq-27.1.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ce980af330231615756acd5154f29813d553ea555485ae712c491cd483df6b7a", size = 1341197, upload-time = "2025-09-08T23:08:44.973Z" }, + { url = "https://files.pythonhosted.org/packages/48/43/d72ccdbf0d73d1343936296665826350cb1e825f92f2db9db3e61c2162a2/pyzmq-27.1.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1779be8c549e54a1c38f805e56d2a2e5c009d26de10921d7d51cfd1c8d4632ea", size = 897175, upload-time = "2025-09-08T23:08:46.601Z" }, + { url = "https://files.pythonhosted.org/packages/2f/2e/a483f73a10b65a9ef0161e817321d39a770b2acf8bcf3004a28d90d14a94/pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7200bb0f03345515df50d99d3db206a0a6bee1955fbb8c453c76f5bf0e08fb96", size = 660427, upload-time = "2025-09-08T23:08:48.187Z" }, + { url = "https://files.pythonhosted.org/packages/f5/d2/5f36552c2d3e5685abe60dfa56f91169f7a2d99bbaf67c5271022ab40863/pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01c0e07d558b06a60773744ea6251f769cd79a41a97d11b8bf4ab8f034b0424d", size = 847929, upload-time = "2025-09-08T23:08:49.76Z" }, + { url = "https://files.pythonhosted.org/packages/c4/2a/404b331f2b7bf3198e9945f75c4c521f0c6a3a23b51f7a4a401b94a13833/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:80d834abee71f65253c91540445d37c4c561e293ba6e741b992f20a105d69146", size = 1650193, upload-time = "2025-09-08T23:08:51.7Z" }, + { url = "https://files.pythonhosted.org/packages/1c/0b/f4107e33f62a5acf60e3ded67ed33d79b4ce18de432625ce2fc5093d6388/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:544b4e3b7198dde4a62b8ff6685e9802a9a1ebf47e77478a5eb88eca2a82f2fd", size = 2024388, upload-time = "2025-09-08T23:08:53.393Z" }, + { url = "https://files.pythonhosted.org/packages/0d/01/add31fe76512642fd6e40e3a3bd21f4b47e242c8ba33efb6809e37076d9b/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cedc4c68178e59a4046f97eca31b148ddcf51e88677de1ef4e78cf06c5376c9a", size = 1885316, upload-time = "2025-09-08T23:08:55.702Z" }, + { url = "https://files.pythonhosted.org/packages/c4/59/a5f38970f9bf07cee96128de79590bb354917914a9be11272cfc7ff26af0/pyzmq-27.1.0-cp314-cp314t-win32.whl", hash = "sha256:1f0b2a577fd770aa6f053211a55d1c47901f4d537389a034c690291485e5fe92", size = 587472, upload-time = "2025-09-08T23:08:58.18Z" }, + { url = "https://files.pythonhosted.org/packages/70/d8/78b1bad170f93fcf5e3536e70e8fadac55030002275c9a29e8f5719185de/pyzmq-27.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:19c9468ae0437f8074af379e986c5d3d7d7bfe033506af442e8c879732bedbe0", size = 661401, upload-time = "2025-09-08T23:08:59.802Z" }, + { url = "https://files.pythonhosted.org/packages/81/d6/4bfbb40c9a0b42fc53c7cf442f6385db70b40f74a783130c5d0a5aa62228/pyzmq-27.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dc5dbf68a7857b59473f7df42650c621d7e8923fb03fa74a526890f4d33cc4d7", size = 575170, upload-time = "2025-09-08T23:09:01.418Z" }, + { url = "https://files.pythonhosted.org/packages/4c/c6/c4dcdecdbaa70969ee1fdced6d7b8f60cfabe64d25361f27ac4665a70620/pyzmq-27.1.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:18770c8d3563715387139060d37859c02ce40718d1faf299abddcdcc6a649066", size = 836265, upload-time = "2025-09-08T23:09:49.376Z" }, + { url = "https://files.pythonhosted.org/packages/3e/79/f38c92eeaeb03a2ccc2ba9866f0439593bb08c5e3b714ac1d553e5c96e25/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:ac25465d42f92e990f8d8b0546b01c391ad431c3bf447683fdc40565941d0604", size = 800208, upload-time = "2025-09-08T23:09:51.073Z" }, + { url = "https://files.pythonhosted.org/packages/49/0e/3f0d0d335c6b3abb9b7b723776d0b21fa7f3a6c819a0db6097059aada160/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53b40f8ae006f2734ee7608d59ed661419f087521edbfc2149c3932e9c14808c", size = 567747, upload-time = "2025-09-08T23:09:52.698Z" }, + { url = "https://files.pythonhosted.org/packages/a1/cf/f2b3784d536250ffd4be70e049f3b60981235d70c6e8ce7e3ef21e1adb25/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f605d884e7c8be8fe1aa94e0a783bf3f591b84c24e4bc4f3e7564c82ac25e271", size = 747371, upload-time = "2025-09-08T23:09:54.563Z" }, + { url = "https://files.pythonhosted.org/packages/01/1b/5dbe84eefc86f48473947e2f41711aded97eecef1231f4558f1f02713c12/pyzmq-27.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c9f7f6e13dff2e44a6afeaf2cf54cee5929ad64afaf4d40b50f93c58fc687355", size = 544862, upload-time = "2025-09-08T23:09:56.509Z" }, +] + +[[package]] +name = "referencing" +version = "0.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, +] + +[[package]] +name = "rich" +version = "15.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, +] + +[[package]] +name = "rpds-py" +version = "0.30.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/6e/f964e88b3d2abee2a82c1ac8366da848fce1c6d834dc2132c3fda3970290/rpds_py-0.30.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425", size = 370157, upload-time = "2025-11-30T20:21:53.789Z" }, + { url = "https://files.pythonhosted.org/packages/94/ba/24e5ebb7c1c82e74c4e4f33b2112a5573ddc703915b13a073737b59b86e0/rpds_py-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d", size = 359676, upload-time = "2025-11-30T20:21:55.475Z" }, + { url = "https://files.pythonhosted.org/packages/84/86/04dbba1b087227747d64d80c3b74df946b986c57af0a9f0c98726d4d7a3b/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4", size = 389938, upload-time = "2025-11-30T20:21:57.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/bb/1463f0b1722b7f45431bdd468301991d1328b16cffe0b1c2918eba2c4eee/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f", size = 402932, upload-time = "2025-11-30T20:21:58.47Z" }, + { url = "https://files.pythonhosted.org/packages/99/ee/2520700a5c1f2d76631f948b0736cdf9b0acb25abd0ca8e889b5c62ac2e3/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4", size = 525830, upload-time = "2025-11-30T20:21:59.699Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ad/bd0331f740f5705cc555a5e17fdf334671262160270962e69a2bdef3bf76/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97", size = 412033, upload-time = "2025-11-30T20:22:00.991Z" }, + { url = "https://files.pythonhosted.org/packages/f8/1e/372195d326549bb51f0ba0f2ecb9874579906b97e08880e7a65c3bef1a99/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89", size = 390828, upload-time = "2025-11-30T20:22:02.723Z" }, + { url = "https://files.pythonhosted.org/packages/ab/2b/d88bb33294e3e0c76bc8f351a3721212713629ffca1700fa94979cb3eae8/rpds_py-0.30.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d", size = 404683, upload-time = "2025-11-30T20:22:04.367Z" }, + { url = "https://files.pythonhosted.org/packages/50/32/c759a8d42bcb5289c1fac697cd92f6fe01a018dd937e62ae77e0e7f15702/rpds_py-0.30.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038", size = 421583, upload-time = "2025-11-30T20:22:05.814Z" }, + { url = "https://files.pythonhosted.org/packages/2b/81/e729761dbd55ddf5d84ec4ff1f47857f4374b0f19bdabfcf929164da3e24/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7", size = 572496, upload-time = "2025-11-30T20:22:07.713Z" }, + { url = "https://files.pythonhosted.org/packages/14/f6/69066a924c3557c9c30baa6ec3a0aa07526305684c6f86c696b08860726c/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed", size = 598669, upload-time = "2025-11-30T20:22:09.312Z" }, + { url = "https://files.pythonhosted.org/packages/5f/48/905896b1eb8a05630d20333d1d8ffd162394127b74ce0b0784ae04498d32/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85", size = 561011, upload-time = "2025-11-30T20:22:11.309Z" }, + { url = "https://files.pythonhosted.org/packages/22/16/cd3027c7e279d22e5eb431dd3c0fbc677bed58797fe7581e148f3f68818b/rpds_py-0.30.0-cp311-cp311-win32.whl", hash = "sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c", size = 221406, upload-time = "2025-11-30T20:22:13.101Z" }, + { url = "https://files.pythonhosted.org/packages/fa/5b/e7b7aa136f28462b344e652ee010d4de26ee9fd16f1bfd5811f5153ccf89/rpds_py-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825", size = 236024, upload-time = "2025-11-30T20:22:14.853Z" }, + { url = "https://files.pythonhosted.org/packages/14/a6/364bba985e4c13658edb156640608f2c9e1d3ea3c81b27aa9d889fff0e31/rpds_py-0.30.0-cp311-cp311-win_arm64.whl", hash = "sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229", size = 229069, upload-time = "2025-11-30T20:22:16.577Z" }, + { url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086, upload-time = "2025-11-30T20:22:17.93Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053, upload-time = "2025-11-30T20:22:19.297Z" }, + { url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763, upload-time = "2025-11-30T20:22:21.661Z" }, + { url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951, upload-time = "2025-11-30T20:22:23.408Z" }, + { url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622, upload-time = "2025-11-30T20:22:25.16Z" }, + { url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492, upload-time = "2025-11-30T20:22:26.505Z" }, + { url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080, upload-time = "2025-11-30T20:22:27.934Z" }, + { url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680, upload-time = "2025-11-30T20:22:29.341Z" }, + { url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589, upload-time = "2025-11-30T20:22:31.469Z" }, + { url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289, upload-time = "2025-11-30T20:22:32.997Z" }, + { url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737, upload-time = "2025-11-30T20:22:34.419Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120, upload-time = "2025-11-30T20:22:35.903Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782, upload-time = "2025-11-30T20:22:37.271Z" }, + { url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463, upload-time = "2025-11-30T20:22:39.021Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868, upload-time = "2025-11-30T20:22:40.493Z" }, + { url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887, upload-time = "2025-11-30T20:22:41.812Z" }, + { url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904, upload-time = "2025-11-30T20:22:43.479Z" }, + { url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945, upload-time = "2025-11-30T20:22:44.819Z" }, + { url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783, upload-time = "2025-11-30T20:22:46.103Z" }, + { url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021, upload-time = "2025-11-30T20:22:47.458Z" }, + { url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589, upload-time = "2025-11-30T20:22:48.872Z" }, + { url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025, upload-time = "2025-11-30T20:22:50.196Z" }, + { url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895, upload-time = "2025-11-30T20:22:51.87Z" }, + { url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799, upload-time = "2025-11-30T20:22:53.341Z" }, + { url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731, upload-time = "2025-11-30T20:22:54.778Z" }, + { url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027, upload-time = "2025-11-30T20:22:56.212Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020, upload-time = "2025-11-30T20:22:58.2Z" }, + { url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139, upload-time = "2025-11-30T20:23:00.209Z" }, + { url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224, upload-time = "2025-11-30T20:23:02.008Z" }, + { url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645, upload-time = "2025-11-30T20:23:03.43Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443, upload-time = "2025-11-30T20:23:04.878Z" }, + { url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375, upload-time = "2025-11-30T20:23:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850, upload-time = "2025-11-30T20:23:07.825Z" }, + { url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812, upload-time = "2025-11-30T20:23:09.228Z" }, + { url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841, upload-time = "2025-11-30T20:23:11.186Z" }, + { url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149, upload-time = "2025-11-30T20:23:12.864Z" }, + { url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843, upload-time = "2025-11-30T20:23:14.638Z" }, + { url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507, upload-time = "2025-11-30T20:23:16.105Z" }, + { url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949, upload-time = "2025-11-30T20:23:17.539Z" }, + { url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790, upload-time = "2025-11-30T20:23:19.029Z" }, + { url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217, upload-time = "2025-11-30T20:23:20.885Z" }, + { url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" }, + { url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341, upload-time = "2025-11-30T20:23:24.449Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768, upload-time = "2025-11-30T20:23:25.908Z" }, + { url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099, upload-time = "2025-11-30T20:23:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192, upload-time = "2025-11-30T20:23:29.151Z" }, + { url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080, upload-time = "2025-11-30T20:23:30.785Z" }, + { url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841, upload-time = "2025-11-30T20:23:32.209Z" }, + { url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670, upload-time = "2025-11-30T20:23:33.742Z" }, + { url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005, upload-time = "2025-11-30T20:23:35.253Z" }, + { url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112, upload-time = "2025-11-30T20:23:36.842Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049, upload-time = "2025-11-30T20:23:38.343Z" }, + { url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661, upload-time = "2025-11-30T20:23:40.263Z" }, + { url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606, upload-time = "2025-11-30T20:23:42.186Z" }, + { url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126, upload-time = "2025-11-30T20:23:44.086Z" }, + { url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371, upload-time = "2025-11-30T20:23:46.004Z" }, + { url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298, upload-time = "2025-11-30T20:23:47.696Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604, upload-time = "2025-11-30T20:23:49.501Z" }, + { url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391, upload-time = "2025-11-30T20:23:50.96Z" }, + { url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868, upload-time = "2025-11-30T20:23:52.494Z" }, + { url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747, upload-time = "2025-11-30T20:23:54.036Z" }, + { url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795, upload-time = "2025-11-30T20:23:55.556Z" }, + { url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330, upload-time = "2025-11-30T20:23:57.033Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194, upload-time = "2025-11-30T20:23:58.637Z" }, + { url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340, upload-time = "2025-11-30T20:24:00.2Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765, upload-time = "2025-11-30T20:24:01.759Z" }, + { url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834, upload-time = "2025-11-30T20:24:03.687Z" }, + { url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470, upload-time = "2025-11-30T20:24:05.232Z" }, + { url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630, upload-time = "2025-11-30T20:24:06.878Z" }, + { url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148, upload-time = "2025-11-30T20:24:08.445Z" }, + { url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030, upload-time = "2025-11-30T20:24:10.956Z" }, + { url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570, upload-time = "2025-11-30T20:24:12.735Z" }, + { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" }, + { url = "https://files.pythonhosted.org/packages/69/71/3f34339ee70521864411f8b6992e7ab13ac30d8e4e3309e07c7361767d91/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58", size = 372292, upload-time = "2025-11-30T20:24:16.537Z" }, + { url = "https://files.pythonhosted.org/packages/57/09/f183df9b8f2d66720d2ef71075c59f7e1b336bec7ee4c48f0a2b06857653/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a", size = 362128, upload-time = "2025-11-30T20:24:18.086Z" }, + { url = "https://files.pythonhosted.org/packages/7a/68/5c2594e937253457342e078f0cc1ded3dd7b2ad59afdbf2d354869110a02/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb", size = 391542, upload-time = "2025-11-30T20:24:20.092Z" }, + { url = "https://files.pythonhosted.org/packages/49/5c/31ef1afd70b4b4fbdb2800249f34c57c64beb687495b10aec0365f53dfc4/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c", size = 404004, upload-time = "2025-11-30T20:24:22.231Z" }, + { url = "https://files.pythonhosted.org/packages/e3/63/0cfbea38d05756f3440ce6534d51a491d26176ac045e2707adc99bb6e60a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3", size = 527063, upload-time = "2025-11-30T20:24:24.302Z" }, + { url = "https://files.pythonhosted.org/packages/42/e6/01e1f72a2456678b0f618fc9a1a13f882061690893c192fcad9f2926553a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5", size = 413099, upload-time = "2025-11-30T20:24:25.916Z" }, + { url = "https://files.pythonhosted.org/packages/b8/25/8df56677f209003dcbb180765520c544525e3ef21ea72279c98b9aa7c7fb/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738", size = 392177, upload-time = "2025-11-30T20:24:27.834Z" }, + { url = "https://files.pythonhosted.org/packages/4a/b4/0a771378c5f16f8115f796d1f437950158679bcd2a7c68cf251cfb00ed5b/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f", size = 406015, upload-time = "2025-11-30T20:24:29.457Z" }, + { url = "https://files.pythonhosted.org/packages/36/d8/456dbba0af75049dc6f63ff295a2f92766b9d521fa00de67a2bd6427d57a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877", size = 423736, upload-time = "2025-11-30T20:24:31.22Z" }, + { url = "https://files.pythonhosted.org/packages/13/64/b4d76f227d5c45a7e0b796c674fd81b0a6c4fbd48dc29271857d8219571c/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a", size = 573981, upload-time = "2025-11-30T20:24:32.934Z" }, + { url = "https://files.pythonhosted.org/packages/20/91/092bacadeda3edf92bf743cc96a7be133e13a39cdbfd7b5082e7ab638406/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4", size = 599782, upload-time = "2025-11-30T20:24:35.169Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b7/b95708304cd49b7b6f82fdd039f1748b66ec2b21d6a45180910802f1abf1/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e", size = 562191, upload-time = "2025-11-30T20:24:36.853Z" }, +] + +[[package]] +name = "ruff" +version = "0.15.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/8d/192f3d7103816158dfd5ea50d098ef2aec19194e6cbccd4b3485bdb2eb2d/ruff-0.15.11.tar.gz", hash = "sha256:f092b21708bf0e7437ce9ada249dfe688ff9a0954fc94abab05dcea7dcd29c33", size = 4637264, upload-time = "2026-04-16T18:46:26.58Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/1e/6aca3427f751295ab011828e15e9bf452200ac74484f1db4be0197b8170b/ruff-0.15.11-py3-none-linux_armv6l.whl", hash = "sha256:e927cfff503135c558eb581a0c9792264aae9507904eb27809cdcff2f2c847b7", size = 10607943, upload-time = "2026-04-16T18:46:05.967Z" }, + { url = "https://files.pythonhosted.org/packages/e7/26/1341c262e74f36d4e84f3d6f4df0ac68cd53331a66bfc5080daa17c84c0b/ruff-0.15.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7a1b5b2938d8f890b76084d4fa843604d787a912541eae85fd7e233398bbb73e", size = 10988592, upload-time = "2026-04-16T18:46:00.742Z" }, + { url = "https://files.pythonhosted.org/packages/03/71/850b1d6ffa9564fbb6740429bad53df1094082fe515c8c1e74b6d8d05f18/ruff-0.15.11-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d4176f3d194afbdaee6e41b9ccb1a2c287dba8700047df474abfbe773825d1cb", size = 10338501, upload-time = "2026-04-16T18:46:03.723Z" }, + { url = "https://files.pythonhosted.org/packages/f2/11/cc1284d3e298c45a817a6aadb6c3e1d70b45c9b36d8d9cce3387b495a03a/ruff-0.15.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b17c886fb88203ced3afe7f14e8d5ae96e9d2f4ccc0ee66aa19f2c2675a27e4", size = 10670693, upload-time = "2026-04-16T18:46:41.941Z" }, + { url = "https://files.pythonhosted.org/packages/ce/9e/f8288b034ab72b371513c13f9a41d9ba3effac54e24bfb467b007daee2ca/ruff-0.15.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:49fafa220220afe7758a487b048de4c8f9f767f37dfefad46b9dd06759d003eb", size = 10416177, upload-time = "2026-04-16T18:46:21.717Z" }, + { url = "https://files.pythonhosted.org/packages/85/71/504d79abfd3d92532ba6bbe3d1c19fada03e494332a59e37c7c2dabae427/ruff-0.15.11-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2ab8427e74a00d93b8bda1307b1e60970d40f304af38bccb218e056c220120d", size = 11221886, upload-time = "2026-04-16T18:46:15.086Z" }, + { url = "https://files.pythonhosted.org/packages/43/5a/947e6ab7a5ad603d65b474be15a4cbc6d29832db5d762cd142e4e3a74164/ruff-0.15.11-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:195072c0c8e1fc8f940652073df082e37a5d9cb43b4ab1e4d0566ab8977a13b7", size = 12075183, upload-time = "2026-04-16T18:46:07.944Z" }, + { url = "https://files.pythonhosted.org/packages/9f/a1/0b7bb6268775fdd3a0818aee8efd8f5b4e231d24dd4d528ced2534023182/ruff-0.15.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a3a0996d486af3920dec930a2e7daed4847dfc12649b537a9335585ada163e9e", size = 11516575, upload-time = "2026-04-16T18:46:31.687Z" }, + { url = "https://files.pythonhosted.org/packages/30/c3/bb5168fc4d233cc06e95f482770d0f3c87945a0cd9f614b90ea8dc2f2833/ruff-0.15.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bef2cb556d509259f1fe440bb9cd33c756222cf0a7afe90d15edf0866702431", size = 11306537, upload-time = "2026-04-16T18:46:36.988Z" }, + { url = "https://files.pythonhosted.org/packages/e4/92/4cfae6441f3967317946f3b788136eecf093729b94d6561f963ed810c82e/ruff-0.15.11-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:030d921a836d7d4a12cf6e8d984a88b66094ccb0e0f17ddd55067c331191bf19", size = 11296813, upload-time = "2026-04-16T18:46:24.182Z" }, + { url = "https://files.pythonhosted.org/packages/43/26/972784c5dde8313acde8ac71ba8ac65475b85db4a2352a76c9934361f9bc/ruff-0.15.11-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:0e783b599b4577788dbbb66b9addcef87e9a8832f4ce0c19e34bf55543a2f890", size = 10633136, upload-time = "2026-04-16T18:46:39.802Z" }, + { url = "https://files.pythonhosted.org/packages/5b/53/3985a4f185020c2f367f2e08a103032e12564829742a1b417980ce1514a0/ruff-0.15.11-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ae90592246625ba4a34349d68ec28d4400d75182b71baa196ddb9f82db025ef5", size = 10424701, upload-time = "2026-04-16T18:46:10.381Z" }, + { url = "https://files.pythonhosted.org/packages/d3/57/bf0dfb32241b56c83bb663a826133da4bf17f682ba8c096973065f6e6a68/ruff-0.15.11-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1f111d62e3c983ed20e0ca2e800f8d77433a5b1161947df99a5c2a3fb60514f0", size = 10873887, upload-time = "2026-04-16T18:46:29.157Z" }, + { url = "https://files.pythonhosted.org/packages/02/05/e48076b2a57dc33ee8c7a957296f97c744ca891a8ffb4ffb1aaa3b3f517d/ruff-0.15.11-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:06f483d6646f59eaffba9ae30956370d3a886625f511a3108994000480621d1c", size = 11404316, upload-time = "2026-04-16T18:46:19.462Z" }, + { url = "https://files.pythonhosted.org/packages/88/27/0195d15fe7a897cbcba0904792c4b7c9fdd958456c3a17d2ea6093716a9a/ruff-0.15.11-py3-none-win32.whl", hash = "sha256:476a2aa56b7da0b73a3ee80b6b2f0e19cce544245479adde7baa65466664d5f3", size = 10655535, upload-time = "2026-04-16T18:46:12.47Z" }, + { url = "https://files.pythonhosted.org/packages/3a/5e/c927b325bd4c1d3620211a4b96f47864633199feed60fa936025ab27e090/ruff-0.15.11-py3-none-win_amd64.whl", hash = "sha256:8b6756d88d7e234fb0c98c91511aae3cd519d5e3ed271cae31b20f39cb2a12a3", size = 11779692, upload-time = "2026-04-16T18:46:17.268Z" }, + { url = "https://files.pythonhosted.org/packages/63/b6/aeadee5443e49baa2facd51131159fd6301cc4ccfc1541e4df7b021c37dd/ruff-0.15.11-py3-none-win_arm64.whl", hash = "sha256:063fed18cc1bbe0ee7393957284a6fe8b588c6a406a285af3ee3f46da2391ee4", size = 11032614, upload-time = "2026-04-16T18:46:34.487Z" }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + +[[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, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "soupsieve" +version = "2.8.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/ae/2d9c981590ed9999a0d91755b47fc74f74de286b0f5cee14c9269041e6c4/soupsieve-2.8.3.tar.gz", hash = "sha256:3267f1eeea4251fb42728b6dfb746edc9acaffc4a45b27e19450b676586e8349", size = 118627, upload-time = "2026-01-20T04:27:02.457Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/2c/1462b1d0a634697ae9e55b3cecdcb64788e8b7d63f54d923fcd0bb140aed/soupsieve-2.8.3-py3-none-any.whl", hash = "sha256:ed64f2ba4eebeab06cc4962affce381647455978ffc1e36bb79a545b91f45a95", size = 37016, upload-time = "2026-01-20T04:27:01.012Z" }, +] + +[[package]] +name = "tinycss2" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "webencodings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7a/fd/7a5ee21fd08ff70d3d33a5781c255cbe779659bd03278feb98b19ee550f4/tinycss2-1.4.0.tar.gz", hash = "sha256:10c0972f6fc0fbee87c3edb76549357415e94548c1ae10ebccdea16fb404a9b7", size = 87085, upload-time = "2024-10-24T14:58:29.895Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/34/ebdc18bae6aa14fbee1a08b63c015c72b64868ff7dae68808ab500c492e2/tinycss2-1.4.0-py3-none-any.whl", hash = "sha256:3a49cf47b7675da0b15d0c6e1df8df4ebd96e9394bb905a5775adb0d884c5289", size = 26610, upload-time = "2024-10-24T14:58:28.029Z" }, +] + +[[package]] +name = "tomli-w" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/19/75/241269d1da26b624c0d5e110e8149093c759b7a286138f4efd61a60e75fe/tomli_w-1.2.0.tar.gz", hash = "sha256:2dd14fac5a47c27be9cd4c976af5a12d87fb1f0b4512f81d69cce3b35ae25021", size = 7184, upload-time = "2025-01-15T12:07:24.262Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl", hash = "sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90", size = 6675, upload-time = "2025-01-15T12:07:22.074Z" }, +] + +[[package]] +name = "tornado" +version = "6.5.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/f1/3173dfa4a18db4a9b03e5d55325559dab51ee653763bb8745a75af491286/tornado-6.5.5.tar.gz", hash = "sha256:192b8f3ea91bd7f1f50c06955416ed76c6b72f96779b962f07f911b91e8d30e9", size = 516006, upload-time = "2026-03-10T21:31:02.067Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/8c/77f5097695f4dd8255ecbd08b2a1ed8ba8b953d337804dd7080f199e12bf/tornado-6.5.5-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:487dc9cc380e29f58c7ab88f9e27cdeef04b2140862e5076a66fb6bb68bb1bfa", size = 445983, upload-time = "2026-03-10T21:30:44.28Z" }, + { url = "https://files.pythonhosted.org/packages/ab/5e/7625b76cd10f98f1516c36ce0346de62061156352353ef2da44e5c21523c/tornado-6.5.5-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:65a7f1d46d4bb41df1ac99f5fcb685fb25c7e61613742d5108b010975a9a6521", size = 444246, upload-time = "2026-03-10T21:30:46.571Z" }, + { url = "https://files.pythonhosted.org/packages/b2/04/7b5705d5b3c0fab088f434f9c83edac1573830ca49ccf29fb83bf7178eec/tornado-6.5.5-cp39-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e74c92e8e65086b338fd56333fb9a68b9f6f2fe7ad532645a290a464bcf46be5", size = 447229, upload-time = "2026-03-10T21:30:48.273Z" }, + { url = "https://files.pythonhosted.org/packages/34/01/74e034a30ef59afb4097ef8659515e96a39d910b712a89af76f5e4e1f93c/tornado-6.5.5-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:435319e9e340276428bbdb4e7fa732c2d399386d1de5686cb331ec8eee754f07", size = 448192, upload-time = "2026-03-10T21:30:51.22Z" }, + { url = "https://files.pythonhosted.org/packages/be/00/fe9e02c5a96429fce1a1d15a517f5d8444f9c412e0bb9eadfbe3b0fc55bf/tornado-6.5.5-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:3f54aa540bdbfee7b9eb268ead60e7d199de5021facd276819c193c0fb28ea4e", size = 448039, upload-time = "2026-03-10T21:30:53.52Z" }, + { url = "https://files.pythonhosted.org/packages/82/9e/656ee4cec0398b1d18d0f1eb6372c41c6b889722641d84948351ae19556d/tornado-6.5.5-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:36abed1754faeb80fbd6e64db2758091e1320f6bba74a4cf8c09cd18ccce8aca", size = 447445, upload-time = "2026-03-10T21:30:55.541Z" }, + { url = "https://files.pythonhosted.org/packages/5a/76/4921c00511f88af86a33de770d64141170f1cfd9c00311aea689949e274e/tornado-6.5.5-cp39-abi3-win32.whl", hash = "sha256:dd3eafaaeec1c7f2f8fdcd5f964e8907ad788fe8a5a32c4426fbbdda621223b7", size = 448582, upload-time = "2026-03-10T21:30:57.142Z" }, + { url = "https://files.pythonhosted.org/packages/2c/23/f6c6112a04d28eed765e374435fb1a9198f73e1ec4b4024184f21faeb1ad/tornado-6.5.5-cp39-abi3-win_amd64.whl", hash = "sha256:6443a794ba961a9f619b1ae926a2e900ac20c34483eea67be4ed8f1e58d3ef7b", size = 448990, upload-time = "2026-03-10T21:30:58.857Z" }, + { url = "https://files.pythonhosted.org/packages/b7/c8/876602cbc96469911f0939f703453c1157b0c826ecb05bdd32e023397d4e/tornado-6.5.5-cp39-abi3-win_arm64.whl", hash = "sha256:2c9a876e094109333f888539ddb2de4361743e5d21eece20688e3e351e4990a6", size = 448016, upload-time = "2026-03-10T21:31:00.43Z" }, +] + +[[package]] +name = "traitlets" +version = "5.14.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/eb/79/72064e6a701c2183016abbbfedaba506d81e30e232a68c9f0d6f6fcd1574/traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7", size = 161621, upload-time = "2024-04-19T11:11:49.746Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359, upload-time = "2024-04-19T11:11:46.763Z" }, +] + +[[package]] +name = "typer" +version = "0.24.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/24/cb09efec5cc954f7f9b930bf8279447d24618bb6758d4f6adf2574c41780/typer-0.24.1.tar.gz", hash = "sha256:e39b4732d65fbdcde189ae76cf7cd48aeae72919dea1fdfc16593be016256b45", size = 118613, upload-time = "2026-02-21T16:54:40.609Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/91/48db081e7a63bb37284f9fbcefda7c44c277b18b0e13fbc36ea2335b71e6/typer-0.24.1-py3-none-any.whl", hash = "sha256:112c1f0ce578bfb4cab9ffdabc68f031416ebcc216536611ba21f04e9aa84c9e", size = 56085, upload-time = "2026-02-21T16:54:41.616Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "webencodings" +version = "0.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/02/ae6ceac1baeda530866a85075641cec12989bd8d31af6d5ab4a3e8c92f47/webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923", size = 9721, upload-time = "2017-04-05T20:21:34.189Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", size = 11774, upload-time = "2017-04-05T20:21:32.581Z" }, +] + +[[package]] +name = "websockets" +version = "16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", size = 179346, upload-time = "2026-01-10T09:23:47.181Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/db/de907251b4ff46ae804ad0409809504153b3f30984daf82a1d84a9875830/websockets-16.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:31a52addea25187bde0797a97d6fc3d2f92b6f72a9370792d65a6e84615ac8a8", size = 177340, upload-time = "2026-01-10T09:22:34.539Z" }, + { url = "https://files.pythonhosted.org/packages/f3/fa/abe89019d8d8815c8781e90d697dec52523fb8ebe308bf11664e8de1877e/websockets-16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:417b28978cdccab24f46400586d128366313e8a96312e4b9362a4af504f3bbad", size = 175022, upload-time = "2026-01-10T09:22:36.332Z" }, + { url = "https://files.pythonhosted.org/packages/58/5d/88ea17ed1ded2079358b40d31d48abe90a73c9e5819dbcde1606e991e2ad/websockets-16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:af80d74d4edfa3cb9ed973a0a5ba2b2a549371f8a741e0800cb07becdd20f23d", size = 175319, upload-time = "2026-01-10T09:22:37.602Z" }, + { url = "https://files.pythonhosted.org/packages/d2/ae/0ee92b33087a33632f37a635e11e1d99d429d3d323329675a6022312aac2/websockets-16.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:08d7af67b64d29823fed316505a89b86705f2b7981c07848fb5e3ea3020c1abe", size = 184631, upload-time = "2026-01-10T09:22:38.789Z" }, + { url = "https://files.pythonhosted.org/packages/c8/c5/27178df583b6c5b31b29f526ba2da5e2f864ecc79c99dae630a85d68c304/websockets-16.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7be95cfb0a4dae143eaed2bcba8ac23f4892d8971311f1b06f3c6b78952ee70b", size = 185870, upload-time = "2026-01-10T09:22:39.893Z" }, + { url = "https://files.pythonhosted.org/packages/87/05/536652aa84ddc1c018dbb7e2c4cbcd0db884580bf8e95aece7593fde526f/websockets-16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d6297ce39ce5c2e6feb13c1a996a2ded3b6832155fcfc920265c76f24c7cceb5", size = 185361, upload-time = "2026-01-10T09:22:41.016Z" }, + { url = "https://files.pythonhosted.org/packages/6d/e2/d5332c90da12b1e01f06fb1b85c50cfc489783076547415bf9f0a659ec19/websockets-16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1c1b30e4f497b0b354057f3467f56244c603a79c0d1dafce1d16c283c25f6e64", size = 184615, upload-time = "2026-01-10T09:22:42.442Z" }, + { url = "https://files.pythonhosted.org/packages/77/fb/d3f9576691cae9253b51555f841bc6600bf0a983a461c79500ace5a5b364/websockets-16.0-cp311-cp311-win32.whl", hash = "sha256:5f451484aeb5cafee1ccf789b1b66f535409d038c56966d6101740c1614b86c6", size = 178246, upload-time = "2026-01-10T09:22:43.654Z" }, + { url = "https://files.pythonhosted.org/packages/54/67/eaff76b3dbaf18dcddabc3b8c1dba50b483761cccff67793897945b37408/websockets-16.0-cp311-cp311-win_amd64.whl", hash = "sha256:8d7f0659570eefb578dacde98e24fb60af35350193e4f56e11190787bee77dac", size = 178684, upload-time = "2026-01-10T09:22:44.941Z" }, + { url = "https://files.pythonhosted.org/packages/84/7b/bac442e6b96c9d25092695578dda82403c77936104b5682307bd4deb1ad4/websockets-16.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:71c989cbf3254fbd5e84d3bff31e4da39c43f884e64f2551d14bb3c186230f00", size = 177365, upload-time = "2026-01-10T09:22:46.787Z" }, + { url = "https://files.pythonhosted.org/packages/b0/fe/136ccece61bd690d9c1f715baaeefd953bb2360134de73519d5df19d29ca/websockets-16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8b6e209ffee39ff1b6d0fa7bfef6de950c60dfb91b8fcead17da4ee539121a79", size = 175038, upload-time = "2026-01-10T09:22:47.999Z" }, + { url = "https://files.pythonhosted.org/packages/40/1e/9771421ac2286eaab95b8575b0cb701ae3663abf8b5e1f64f1fd90d0a673/websockets-16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86890e837d61574c92a97496d590968b23c2ef0aeb8a9bc9421d174cd378ae39", size = 175328, upload-time = "2026-01-10T09:22:49.809Z" }, + { url = "https://files.pythonhosted.org/packages/18/29/71729b4671f21e1eaa5d6573031ab810ad2936c8175f03f97f3ff164c802/websockets-16.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9b5aca38b67492ef518a8ab76851862488a478602229112c4b0d58d63a7a4d5c", size = 184915, upload-time = "2026-01-10T09:22:51.071Z" }, + { url = "https://files.pythonhosted.org/packages/97/bb/21c36b7dbbafc85d2d480cd65df02a1dc93bf76d97147605a8e27ff9409d/websockets-16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e0334872c0a37b606418ac52f6ab9cfd17317ac26365f7f65e203e2d0d0d359f", size = 186152, upload-time = "2026-01-10T09:22:52.224Z" }, + { url = "https://files.pythonhosted.org/packages/4a/34/9bf8df0c0cf88fa7bfe36678dc7b02970c9a7d5e065a3099292db87b1be2/websockets-16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a0b31e0b424cc6b5a04b8838bbaec1688834b2383256688cf47eb97412531da1", size = 185583, upload-time = "2026-01-10T09:22:53.443Z" }, + { url = "https://files.pythonhosted.org/packages/47/88/4dd516068e1a3d6ab3c7c183288404cd424a9a02d585efbac226cb61ff2d/websockets-16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:485c49116d0af10ac698623c513c1cc01c9446c058a4e61e3bf6c19dff7335a2", size = 184880, upload-time = "2026-01-10T09:22:55.033Z" }, + { url = "https://files.pythonhosted.org/packages/91/d6/7d4553ad4bf1c0421e1ebd4b18de5d9098383b5caa1d937b63df8d04b565/websockets-16.0-cp312-cp312-win32.whl", hash = "sha256:eaded469f5e5b7294e2bdca0ab06becb6756ea86894a47806456089298813c89", size = 178261, upload-time = "2026-01-10T09:22:56.251Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f0/f3a17365441ed1c27f850a80b2bc680a0fa9505d733fe152fdf5e98c1c0b/websockets-16.0-cp312-cp312-win_amd64.whl", hash = "sha256:5569417dc80977fc8c2d43a86f78e0a5a22fee17565d78621b6bb264a115d4ea", size = 178693, upload-time = "2026-01-10T09:22:57.478Z" }, + { url = "https://files.pythonhosted.org/packages/cc/9c/baa8456050d1c1b08dd0ec7346026668cbc6f145ab4e314d707bb845bf0d/websockets-16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:878b336ac47938b474c8f982ac2f7266a540adc3fa4ad74ae96fea9823a02cc9", size = 177364, upload-time = "2026-01-10T09:22:59.333Z" }, + { url = "https://files.pythonhosted.org/packages/7e/0c/8811fc53e9bcff68fe7de2bcbe75116a8d959ac699a3200f4847a8925210/websockets-16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:52a0fec0e6c8d9a784c2c78276a48a2bdf099e4ccc2a4cad53b27718dbfd0230", size = 175039, upload-time = "2026-01-10T09:23:01.171Z" }, + { url = "https://files.pythonhosted.org/packages/aa/82/39a5f910cb99ec0b59e482971238c845af9220d3ab9fa76dd9162cda9d62/websockets-16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6578ed5b6981005df1860a56e3617f14a6c307e6a71b4fff8c48fdc50f3ed2c", size = 175323, upload-time = "2026-01-10T09:23:02.341Z" }, + { url = "https://files.pythonhosted.org/packages/bd/28/0a25ee5342eb5d5f297d992a77e56892ecb65e7854c7898fb7d35e9b33bd/websockets-16.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95724e638f0f9c350bb1c2b0a7ad0e83d9cc0c9259f3ea94e40d7b02a2179ae5", size = 184975, upload-time = "2026-01-10T09:23:03.756Z" }, + { url = "https://files.pythonhosted.org/packages/f9/66/27ea52741752f5107c2e41fda05e8395a682a1e11c4e592a809a90c6a506/websockets-16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0204dc62a89dc9d50d682412c10b3542d748260d743500a85c13cd1ee4bde82", size = 186203, upload-time = "2026-01-10T09:23:05.01Z" }, + { url = "https://files.pythonhosted.org/packages/37/e5/8e32857371406a757816a2b471939d51c463509be73fa538216ea52b792a/websockets-16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52ac480f44d32970d66763115edea932f1c5b1312de36df06d6b219f6741eed8", size = 185653, upload-time = "2026-01-10T09:23:06.301Z" }, + { url = "https://files.pythonhosted.org/packages/9b/67/f926bac29882894669368dc73f4da900fcdf47955d0a0185d60103df5737/websockets-16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6e5a82b677f8f6f59e8dfc34ec06ca6b5b48bc4fcda346acd093694cc2c24d8f", size = 184920, upload-time = "2026-01-10T09:23:07.492Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a1/3d6ccdcd125b0a42a311bcd15a7f705d688f73b2a22d8cf1c0875d35d34a/websockets-16.0-cp313-cp313-win32.whl", hash = "sha256:abf050a199613f64c886ea10f38b47770a65154dc37181bfaff70c160f45315a", size = 178255, upload-time = "2026-01-10T09:23:09.245Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ae/90366304d7c2ce80f9b826096a9e9048b4bb760e44d3b873bb272cba696b/websockets-16.0-cp313-cp313-win_amd64.whl", hash = "sha256:3425ac5cf448801335d6fdc7ae1eb22072055417a96cc6b31b3861f455fbc156", size = 178689, upload-time = "2026-01-10T09:23:10.483Z" }, + { url = "https://files.pythonhosted.org/packages/f3/1d/e88022630271f5bd349ed82417136281931e558d628dd52c4d8621b4a0b2/websockets-16.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0", size = 177406, upload-time = "2026-01-10T09:23:12.178Z" }, + { url = "https://files.pythonhosted.org/packages/f2/78/e63be1bf0724eeb4616efb1ae1c9044f7c3953b7957799abb5915bffd38e/websockets-16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:daa3b6ff70a9241cf6c7fc9e949d41232d9d7d26fd3522b1ad2b4d62487e9904", size = 175085, upload-time = "2026-01-10T09:23:13.511Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f4/d3c9220d818ee955ae390cf319a7c7a467beceb24f05ee7aaaa2414345ba/websockets-16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4", size = 175328, upload-time = "2026-01-10T09:23:14.727Z" }, + { url = "https://files.pythonhosted.org/packages/63/bc/d3e208028de777087e6fb2b122051a6ff7bbcca0d6df9d9c2bf1dd869ae9/websockets-16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:781caf5e8eee67f663126490c2f96f40906594cb86b408a703630f95550a8c3e", size = 185044, upload-time = "2026-01-10T09:23:15.939Z" }, + { url = "https://files.pythonhosted.org/packages/ad/6e/9a0927ac24bd33a0a9af834d89e0abc7cfd8e13bed17a86407a66773cc0e/websockets-16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caab51a72c51973ca21fa8a18bd8165e1a0183f1ac7066a182ff27107b71e1a4", size = 186279, upload-time = "2026-01-10T09:23:17.148Z" }, + { url = "https://files.pythonhosted.org/packages/b9/ca/bf1c68440d7a868180e11be653c85959502efd3a709323230314fda6e0b3/websockets-16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1", size = 185711, upload-time = "2026-01-10T09:23:18.372Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f8/fdc34643a989561f217bb477cbc47a3a07212cbda91c0e4389c43c296ebf/websockets-16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5e18a238a2b2249c9a9235466b90e96ae4795672598a58772dd806edc7ac6d3", size = 184982, upload-time = "2026-01-10T09:23:19.652Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d1/574fa27e233764dbac9c52730d63fcf2823b16f0856b3329fc6268d6ae4f/websockets-16.0-cp314-cp314-win32.whl", hash = "sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8", size = 177915, upload-time = "2026-01-10T09:23:21.458Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f1/ae6b937bf3126b5134ce1f482365fde31a357c784ac51852978768b5eff4/websockets-16.0-cp314-cp314-win_amd64.whl", hash = "sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d", size = 178381, upload-time = "2026-01-10T09:23:22.715Z" }, + { url = "https://files.pythonhosted.org/packages/06/9b/f791d1db48403e1f0a27577a6beb37afae94254a8c6f08be4a23e4930bc0/websockets-16.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a35539cacc3febb22b8f4d4a99cc79b104226a756aa7400adc722e83b0d03244", size = 177737, upload-time = "2026-01-10T09:23:24.523Z" }, + { url = "https://files.pythonhosted.org/packages/bd/40/53ad02341fa33b3ce489023f635367a4ac98b73570102ad2cdd770dacc9a/websockets-16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b784ca5de850f4ce93ec85d3269d24d4c82f22b7212023c974c401d4980ebc5e", size = 175268, upload-time = "2026-01-10T09:23:25.781Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/6158d4e459b984f949dcbbb0c5d270154c7618e11c01029b9bbd1bb4c4f9/websockets-16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:569d01a4e7fba956c5ae4fc988f0d4e187900f5497ce46339c996dbf24f17641", size = 175486, upload-time = "2026-01-10T09:23:27.033Z" }, + { url = "https://files.pythonhosted.org/packages/e5/2d/7583b30208b639c8090206f95073646c2c9ffd66f44df967981a64f849ad/websockets-16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50f23cdd8343b984957e4077839841146f67a3d31ab0d00e6b824e74c5b2f6e8", size = 185331, upload-time = "2026-01-10T09:23:28.259Z" }, + { url = "https://files.pythonhosted.org/packages/45/b0/cce3784eb519b7b5ad680d14b9673a31ab8dcb7aad8b64d81709d2430aa8/websockets-16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:152284a83a00c59b759697b7f9e9cddf4e3c7861dd0d964b472b70f78f89e80e", size = 186501, upload-time = "2026-01-10T09:23:29.449Z" }, + { url = "https://files.pythonhosted.org/packages/19/60/b8ebe4c7e89fb5f6cdf080623c9d92789a53636950f7abacfc33fe2b3135/websockets-16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944", size = 186062, upload-time = "2026-01-10T09:23:31.368Z" }, + { url = "https://files.pythonhosted.org/packages/88/a8/a080593f89b0138b6cba1b28f8df5673b5506f72879322288b031337c0b8/websockets-16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206", size = 185356, upload-time = "2026-01-10T09:23:32.627Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b6/b9afed2afadddaf5ebb2afa801abf4b0868f42f8539bfe4b071b5266c9fe/websockets-16.0-cp314-cp314t-win32.whl", hash = "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6", size = 178085, upload-time = "2026-01-10T09:23:33.816Z" }, + { url = "https://files.pythonhosted.org/packages/9f/3e/28135a24e384493fa804216b79a6a6759a38cc4ff59118787b9fb693df93/websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd", size = 178531, upload-time = "2026-01-10T09:23:35.016Z" }, + { url = "https://files.pythonhosted.org/packages/72/07/c98a68571dcf256e74f1f816b8cc5eae6eb2d3d5cfa44d37f801619d9166/websockets-16.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:349f83cd6c9a415428ee1005cadb5c2c56f4389bc06a9af16103c3bc3dcc8b7d", size = 174947, upload-time = "2026-01-10T09:23:36.166Z" }, + { url = "https://files.pythonhosted.org/packages/7e/52/93e166a81e0305b33fe416338be92ae863563fe7bce446b0f687b9df5aea/websockets-16.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:4a1aba3340a8dca8db6eb5a7986157f52eb9e436b74813764241981ca4888f03", size = 175260, upload-time = "2026-01-10T09:23:37.409Z" }, + { url = "https://files.pythonhosted.org/packages/56/0c/2dbf513bafd24889d33de2ff0368190a0e69f37bcfa19009ef819fe4d507/websockets-16.0-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f4a32d1bd841d4bcbffdcb3d2ce50c09c3909fbead375ab28d0181af89fd04da", size = 176071, upload-time = "2026-01-10T09:23:39.158Z" }, + { url = "https://files.pythonhosted.org/packages/a5/8f/aea9c71cc92bf9b6cc0f7f70df8f0b420636b6c96ef4feee1e16f80f75dd/websockets-16.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0298d07ee155e2e9fda5be8a9042200dd2e3bb0b8a38482156576f863a9d457c", size = 176968, upload-time = "2026-01-10T09:23:41.031Z" }, + { url = "https://files.pythonhosted.org/packages/9a/3f/f70e03f40ffc9a30d817eef7da1be72ee4956ba8d7255c399a01b135902a/websockets-16.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:a653aea902e0324b52f1613332ddf50b00c06fdaf7e92624fbf8c77c78fa5767", size = 178735, upload-time = "2026-01-10T09:23:42.259Z" }, + { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" }, +] + +[[package]] +name = "xxhash" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/02/84/30869e01909fb37a6cc7e18688ee8bf1e42d57e7e0777636bd47524c43c7/xxhash-3.6.0.tar.gz", hash = "sha256:f0162a78b13a0d7617b2845b90c763339d1f1d82bb04a4b07f4ab535cc5e05d6", size = 85160, upload-time = "2025-10-02T14:37:08.097Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/d4/cc2f0400e9154df4b9964249da78ebd72f318e35ccc425e9f403c392f22a/xxhash-3.6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b47bbd8cf2d72797f3c2772eaaac0ded3d3af26481a26d7d7d41dc2d3c46b04a", size = 32844, upload-time = "2025-10-02T14:34:14.037Z" }, + { url = "https://files.pythonhosted.org/packages/5e/ec/1cc11cd13e26ea8bc3cb4af4eaadd8d46d5014aebb67be3f71fb0b68802a/xxhash-3.6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2b6821e94346f96db75abaa6e255706fb06ebd530899ed76d32cd99f20dc52fa", size = 30809, upload-time = "2025-10-02T14:34:15.484Z" }, + { url = "https://files.pythonhosted.org/packages/04/5f/19fe357ea348d98ca22f456f75a30ac0916b51c753e1f8b2e0e6fb884cce/xxhash-3.6.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d0a9751f71a1a65ce3584e9cae4467651c7e70c9d31017fa57574583a4540248", size = 194665, upload-time = "2025-10-02T14:34:16.541Z" }, + { url = "https://files.pythonhosted.org/packages/90/3b/d1f1a8f5442a5fd8beedae110c5af7604dc37349a8e16519c13c19a9a2de/xxhash-3.6.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b29ee68625ab37b04c0b40c3fafdf24d2f75ccd778333cfb698f65f6c463f62", size = 213550, upload-time = "2025-10-02T14:34:17.878Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ef/3a9b05eb527457d5db13a135a2ae1a26c80fecd624d20f3e8dcc4cb170f3/xxhash-3.6.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6812c25fe0d6c36a46ccb002f40f27ac903bf18af9f6dd8f9669cb4d176ab18f", size = 212384, upload-time = "2025-10-02T14:34:19.182Z" }, + { url = "https://files.pythonhosted.org/packages/0f/18/ccc194ee698c6c623acbf0f8c2969811a8a4b6185af5e824cd27b9e4fd3e/xxhash-3.6.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4ccbff013972390b51a18ef1255ef5ac125c92dc9143b2d1909f59abc765540e", size = 445749, upload-time = "2025-10-02T14:34:20.659Z" }, + { url = "https://files.pythonhosted.org/packages/a5/86/cf2c0321dc3940a7aa73076f4fd677a0fb3e405cb297ead7d864fd90847e/xxhash-3.6.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:297b7fbf86c82c550e12e8fb71968b3f033d27b874276ba3624ea868c11165a8", size = 193880, upload-time = "2025-10-02T14:34:22.431Z" }, + { url = "https://files.pythonhosted.org/packages/82/fb/96213c8560e6f948a1ecc9a7613f8032b19ee45f747f4fca4eb31bb6d6ed/xxhash-3.6.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dea26ae1eb293db089798d3973a5fc928a18fdd97cc8801226fae705b02b14b0", size = 210912, upload-time = "2025-10-02T14:34:23.937Z" }, + { url = "https://files.pythonhosted.org/packages/40/aa/4395e669b0606a096d6788f40dbdf2b819d6773aa290c19e6e83cbfc312f/xxhash-3.6.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7a0b169aafb98f4284f73635a8e93f0735f9cbde17bd5ec332480484241aaa77", size = 198654, upload-time = "2025-10-02T14:34:25.644Z" }, + { url = "https://files.pythonhosted.org/packages/67/74/b044fcd6b3d89e9b1b665924d85d3f400636c23590226feb1eb09e1176ce/xxhash-3.6.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:08d45aef063a4531b785cd72de4887766d01dc8f362a515693df349fdb825e0c", size = 210867, upload-time = "2025-10-02T14:34:27.203Z" }, + { url = "https://files.pythonhosted.org/packages/bc/fd/3ce73bf753b08cb19daee1eb14aa0d7fe331f8da9c02dd95316ddfe5275e/xxhash-3.6.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:929142361a48ee07f09121fe9e96a84950e8d4df3bb298ca5d88061969f34d7b", size = 414012, upload-time = "2025-10-02T14:34:28.409Z" }, + { url = "https://files.pythonhosted.org/packages/ba/b3/5a4241309217c5c876f156b10778f3ab3af7ba7e3259e6d5f5c7d0129eb2/xxhash-3.6.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:51312c768403d8540487dbbfb557454cfc55589bbde6424456951f7fcd4facb3", size = 191409, upload-time = "2025-10-02T14:34:29.696Z" }, + { url = "https://files.pythonhosted.org/packages/c0/01/99bfbc15fb9abb9a72b088c1d95219fc4782b7d01fc835bd5744d66dd0b8/xxhash-3.6.0-cp311-cp311-win32.whl", hash = "sha256:d1927a69feddc24c987b337ce81ac15c4720955b667fe9b588e02254b80446fd", size = 30574, upload-time = "2025-10-02T14:34:31.028Z" }, + { url = "https://files.pythonhosted.org/packages/65/79/9d24d7f53819fe301b231044ea362ce64e86c74f6e8c8e51320de248b3e5/xxhash-3.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:26734cdc2d4ffe449b41d186bbeac416f704a482ed835d375a5c0cb02bc63fef", size = 31481, upload-time = "2025-10-02T14:34:32.062Z" }, + { url = "https://files.pythonhosted.org/packages/30/4e/15cd0e3e8772071344eab2961ce83f6e485111fed8beb491a3f1ce100270/xxhash-3.6.0-cp311-cp311-win_arm64.whl", hash = "sha256:d72f67ef8bf36e05f5b6c65e8524f265bd61071471cd4cf1d36743ebeeeb06b7", size = 27861, upload-time = "2025-10-02T14:34:33.555Z" }, + { url = "https://files.pythonhosted.org/packages/9a/07/d9412f3d7d462347e4511181dea65e47e0d0e16e26fbee2ea86a2aefb657/xxhash-3.6.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:01362c4331775398e7bb34e3ab403bc9ee9f7c497bc7dee6272114055277dd3c", size = 32744, upload-time = "2025-10-02T14:34:34.622Z" }, + { url = "https://files.pythonhosted.org/packages/79/35/0429ee11d035fc33abe32dca1b2b69e8c18d236547b9a9b72c1929189b9a/xxhash-3.6.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b7b2df81a23f8cb99656378e72501b2cb41b1827c0f5a86f87d6b06b69f9f204", size = 30816, upload-time = "2025-10-02T14:34:36.043Z" }, + { url = "https://files.pythonhosted.org/packages/b7/f2/57eb99aa0f7d98624c0932c5b9a170e1806406cdbcdb510546634a1359e0/xxhash-3.6.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:dc94790144e66b14f67b10ac8ed75b39ca47536bf8800eb7c24b50271ea0c490", size = 194035, upload-time = "2025-10-02T14:34:37.354Z" }, + { url = "https://files.pythonhosted.org/packages/4c/ed/6224ba353690d73af7a3f1c7cdb1fc1b002e38f783cb991ae338e1eb3d79/xxhash-3.6.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:93f107c673bccf0d592cdba077dedaf52fe7f42dcd7676eba1f6d6f0c3efffd2", size = 212914, upload-time = "2025-10-02T14:34:38.6Z" }, + { url = "https://files.pythonhosted.org/packages/38/86/fb6b6130d8dd6b8942cc17ab4d90e223653a89aa32ad2776f8af7064ed13/xxhash-3.6.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2aa5ee3444c25b69813663c9f8067dcfaa2e126dc55e8dddf40f4d1c25d7effa", size = 212163, upload-time = "2025-10-02T14:34:39.872Z" }, + { url = "https://files.pythonhosted.org/packages/ee/dc/e84875682b0593e884ad73b2d40767b5790d417bde603cceb6878901d647/xxhash-3.6.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7f99123f0e1194fa59cc69ad46dbae2e07becec5df50a0509a808f90a0f03f0", size = 445411, upload-time = "2025-10-02T14:34:41.569Z" }, + { url = "https://files.pythonhosted.org/packages/11/4f/426f91b96701ec2f37bb2b8cec664eff4f658a11f3fa9d94f0a887ea6d2b/xxhash-3.6.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:49e03e6fe2cac4a1bc64952dd250cf0dbc5ef4ebb7b8d96bce82e2de163c82a2", size = 193883, upload-time = "2025-10-02T14:34:43.249Z" }, + { url = "https://files.pythonhosted.org/packages/53/5a/ddbb83eee8e28b778eacfc5a85c969673e4023cdeedcfcef61f36731610b/xxhash-3.6.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bd17fede52a17a4f9a7bc4472a5867cb0b160deeb431795c0e4abe158bc784e9", size = 210392, upload-time = "2025-10-02T14:34:45.042Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c2/ff69efd07c8c074ccdf0a4f36fcdd3d27363665bcdf4ba399abebe643465/xxhash-3.6.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:6fb5f5476bef678f69db04f2bd1efbed3030d2aba305b0fc1773645f187d6a4e", size = 197898, upload-time = "2025-10-02T14:34:46.302Z" }, + { url = "https://files.pythonhosted.org/packages/58/ca/faa05ac19b3b622c7c9317ac3e23954187516298a091eb02c976d0d3dd45/xxhash-3.6.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:843b52f6d88071f87eba1631b684fcb4b2068cd2180a0224122fe4ef011a9374", size = 210655, upload-time = "2025-10-02T14:34:47.571Z" }, + { url = "https://files.pythonhosted.org/packages/d4/7a/06aa7482345480cc0cb597f5c875b11a82c3953f534394f620b0be2f700c/xxhash-3.6.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7d14a6cfaf03b1b6f5f9790f76880601ccc7896aff7ab9cd8978a939c1eb7e0d", size = 414001, upload-time = "2025-10-02T14:34:49.273Z" }, + { url = "https://files.pythonhosted.org/packages/23/07/63ffb386cd47029aa2916b3d2f454e6cc5b9f5c5ada3790377d5430084e7/xxhash-3.6.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:418daf3db71e1413cfe211c2f9a528456936645c17f46b5204705581a45390ae", size = 191431, upload-time = "2025-10-02T14:34:50.798Z" }, + { url = "https://files.pythonhosted.org/packages/0f/93/14fde614cadb4ddf5e7cebf8918b7e8fac5ae7861c1875964f17e678205c/xxhash-3.6.0-cp312-cp312-win32.whl", hash = "sha256:50fc255f39428a27299c20e280d6193d8b63b8ef8028995323bf834a026b4fbb", size = 30617, upload-time = "2025-10-02T14:34:51.954Z" }, + { url = "https://files.pythonhosted.org/packages/13/5d/0d125536cbe7565a83d06e43783389ecae0c0f2ed037b48ede185de477c0/xxhash-3.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:c0f2ab8c715630565ab8991b536ecded9416d615538be8ecddce43ccf26cbc7c", size = 31534, upload-time = "2025-10-02T14:34:53.276Z" }, + { url = "https://files.pythonhosted.org/packages/54/85/6ec269b0952ec7e36ba019125982cf11d91256a778c7c3f98a4c5043d283/xxhash-3.6.0-cp312-cp312-win_arm64.whl", hash = "sha256:eae5c13f3bc455a3bbb68bdc513912dc7356de7e2280363ea235f71f54064829", size = 27876, upload-time = "2025-10-02T14:34:54.371Z" }, + { url = "https://files.pythonhosted.org/packages/33/76/35d05267ac82f53ae9b0e554da7c5e281ee61f3cad44c743f0fcd354f211/xxhash-3.6.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:599e64ba7f67472481ceb6ee80fa3bd828fd61ba59fb11475572cc5ee52b89ec", size = 32738, upload-time = "2025-10-02T14:34:55.839Z" }, + { url = "https://files.pythonhosted.org/packages/31/a8/3fbce1cd96534a95e35d5120637bf29b0d7f5d8fa2f6374e31b4156dd419/xxhash-3.6.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7d8b8aaa30fca4f16f0c84a5c8d7ddee0e25250ec2796c973775373257dde8f1", size = 30821, upload-time = "2025-10-02T14:34:57.219Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ea/d387530ca7ecfa183cb358027f1833297c6ac6098223fd14f9782cd0015c/xxhash-3.6.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d597acf8506d6e7101a4a44a5e428977a51c0fadbbfd3c39650cca9253f6e5a6", size = 194127, upload-time = "2025-10-02T14:34:59.21Z" }, + { url = "https://files.pythonhosted.org/packages/ba/0c/71435dcb99874b09a43b8d7c54071e600a7481e42b3e3ce1eb5226a5711a/xxhash-3.6.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:858dc935963a33bc33490128edc1c12b0c14d9c7ebaa4e387a7869ecc4f3e263", size = 212975, upload-time = "2025-10-02T14:35:00.816Z" }, + { url = "https://files.pythonhosted.org/packages/84/7a/c2b3d071e4bb4a90b7057228a99b10d51744878f4a8a6dd643c8bd897620/xxhash-3.6.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba284920194615cb8edf73bf52236ce2e1664ccd4a38fdb543506413529cc546", size = 212241, upload-time = "2025-10-02T14:35:02.207Z" }, + { url = "https://files.pythonhosted.org/packages/81/5f/640b6eac0128e215f177df99eadcd0f1b7c42c274ab6a394a05059694c5a/xxhash-3.6.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4b54219177f6c6674d5378bd862c6aedf64725f70dd29c472eaae154df1a2e89", size = 445471, upload-time = "2025-10-02T14:35:03.61Z" }, + { url = "https://files.pythonhosted.org/packages/5e/1e/3c3d3ef071b051cc3abbe3721ffb8365033a172613c04af2da89d5548a87/xxhash-3.6.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:42c36dd7dbad2f5238950c377fcbf6811b1cdb1c444fab447960030cea60504d", size = 193936, upload-time = "2025-10-02T14:35:05.013Z" }, + { url = "https://files.pythonhosted.org/packages/2c/bd/4a5f68381939219abfe1c22a9e3a5854a4f6f6f3c4983a87d255f21f2e5d/xxhash-3.6.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f22927652cba98c44639ffdc7aaf35828dccf679b10b31c4ad72a5b530a18eb7", size = 210440, upload-time = "2025-10-02T14:35:06.239Z" }, + { url = "https://files.pythonhosted.org/packages/eb/37/b80fe3d5cfb9faff01a02121a0f4d565eb7237e9e5fc66e73017e74dcd36/xxhash-3.6.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b45fad44d9c5c119e9c6fbf2e1c656a46dc68e280275007bbfd3d572b21426db", size = 197990, upload-time = "2025-10-02T14:35:07.735Z" }, + { url = "https://files.pythonhosted.org/packages/d7/fd/2c0a00c97b9e18f72e1f240ad4e8f8a90fd9d408289ba9c7c495ed7dc05c/xxhash-3.6.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:6f2580ffab1a8b68ef2b901cde7e55fa8da5e4be0977c68f78fc80f3c143de42", size = 210689, upload-time = "2025-10-02T14:35:09.438Z" }, + { url = "https://files.pythonhosted.org/packages/93/86/5dd8076a926b9a95db3206aba20d89a7fc14dd5aac16e5c4de4b56033140/xxhash-3.6.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:40c391dd3cd041ebc3ffe6f2c862f402e306eb571422e0aa918d8070ba31da11", size = 414068, upload-time = "2025-10-02T14:35:11.162Z" }, + { url = "https://files.pythonhosted.org/packages/af/3c/0bb129170ee8f3650f08e993baee550a09593462a5cddd8e44d0011102b1/xxhash-3.6.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f205badabde7aafd1a31e8ca2a3e5a763107a71c397c4481d6a804eb5063d8bd", size = 191495, upload-time = "2025-10-02T14:35:12.971Z" }, + { url = "https://files.pythonhosted.org/packages/e9/3a/6797e0114c21d1725e2577508e24006fd7ff1d8c0c502d3b52e45c1771d8/xxhash-3.6.0-cp313-cp313-win32.whl", hash = "sha256:2577b276e060b73b73a53042ea5bd5203d3e6347ce0d09f98500f418a9fcf799", size = 30620, upload-time = "2025-10-02T14:35:14.129Z" }, + { url = "https://files.pythonhosted.org/packages/86/15/9bc32671e9a38b413a76d24722a2bf8784a132c043063a8f5152d390b0f9/xxhash-3.6.0-cp313-cp313-win_amd64.whl", hash = "sha256:757320d45d2fbcce8f30c42a6b2f47862967aea7bf458b9625b4bbe7ee390392", size = 31542, upload-time = "2025-10-02T14:35:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/39/c5/cc01e4f6188656e56112d6a8e0dfe298a16934b8c47a247236549a3f7695/xxhash-3.6.0-cp313-cp313-win_arm64.whl", hash = "sha256:457b8f85dec5825eed7b69c11ae86834a018b8e3df5e77783c999663da2f96d6", size = 27880, upload-time = "2025-10-02T14:35:16.315Z" }, + { url = "https://files.pythonhosted.org/packages/f3/30/25e5321c8732759e930c555176d37e24ab84365482d257c3b16362235212/xxhash-3.6.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a42e633d75cdad6d625434e3468126c73f13f7584545a9cf34e883aa1710e702", size = 32956, upload-time = "2025-10-02T14:35:17.413Z" }, + { url = "https://files.pythonhosted.org/packages/9f/3c/0573299560d7d9f8ab1838f1efc021a280b5ae5ae2e849034ef3dee18810/xxhash-3.6.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:568a6d743219e717b07b4e03b0a828ce593833e498c3b64752e0f5df6bfe84db", size = 31072, upload-time = "2025-10-02T14:35:18.844Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1c/52d83a06e417cd9d4137722693424885cc9878249beb3a7c829e74bf7ce9/xxhash-3.6.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bec91b562d8012dae276af8025a55811b875baace6af510412a5e58e3121bc54", size = 196409, upload-time = "2025-10-02T14:35:20.31Z" }, + { url = "https://files.pythonhosted.org/packages/e3/8e/c6d158d12a79bbd0b878f8355432075fc82759e356ab5a111463422a239b/xxhash-3.6.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78e7f2f4c521c30ad5e786fdd6bae89d47a32672a80195467b5de0480aa97b1f", size = 215736, upload-time = "2025-10-02T14:35:21.616Z" }, + { url = "https://files.pythonhosted.org/packages/bc/68/c4c80614716345d55071a396cf03d06e34b5f4917a467faf43083c995155/xxhash-3.6.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3ed0df1b11a79856df5ffcab572cbd6b9627034c1c748c5566fa79df9048a7c5", size = 214833, upload-time = "2025-10-02T14:35:23.32Z" }, + { url = "https://files.pythonhosted.org/packages/7e/e9/ae27c8ffec8b953efa84c7c4a6c6802c263d587b9fc0d6e7cea64e08c3af/xxhash-3.6.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0e4edbfc7d420925b0dd5e792478ed393d6e75ff8fc219a6546fb446b6a417b1", size = 448348, upload-time = "2025-10-02T14:35:25.111Z" }, + { url = "https://files.pythonhosted.org/packages/d7/6b/33e21afb1b5b3f46b74b6bd1913639066af218d704cc0941404ca717fc57/xxhash-3.6.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fba27a198363a7ef87f8c0f6b171ec36b674fe9053742c58dd7e3201c1ab30ee", size = 196070, upload-time = "2025-10-02T14:35:26.586Z" }, + { url = "https://files.pythonhosted.org/packages/96/b6/fcabd337bc5fa624e7203aa0fa7d0c49eed22f72e93229431752bddc83d9/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:794fe9145fe60191c6532fa95063765529770edcdd67b3d537793e8004cabbfd", size = 212907, upload-time = "2025-10-02T14:35:28.087Z" }, + { url = "https://files.pythonhosted.org/packages/4b/d3/9ee6160e644d660fcf176c5825e61411c7f62648728f69c79ba237250143/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:6105ef7e62b5ac73a837778efc331a591d8442f8ef5c7e102376506cb4ae2729", size = 200839, upload-time = "2025-10-02T14:35:29.857Z" }, + { url = "https://files.pythonhosted.org/packages/0d/98/e8de5baa5109394baf5118f5e72ab21a86387c4f89b0e77ef3e2f6b0327b/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:f01375c0e55395b814a679b3eea205db7919ac2af213f4a6682e01220e5fe292", size = 213304, upload-time = "2025-10-02T14:35:31.222Z" }, + { url = "https://files.pythonhosted.org/packages/7b/1d/71056535dec5c3177eeb53e38e3d367dd1d16e024e63b1cee208d572a033/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:d706dca2d24d834a4661619dcacf51a75c16d65985718d6a7d73c1eeeb903ddf", size = 416930, upload-time = "2025-10-02T14:35:32.517Z" }, + { url = "https://files.pythonhosted.org/packages/dc/6c/5cbde9de2cd967c322e651c65c543700b19e7ae3e0aae8ece3469bf9683d/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5f059d9faeacd49c0215d66f4056e1326c80503f51a1532ca336a385edadd033", size = 193787, upload-time = "2025-10-02T14:35:33.827Z" }, + { url = "https://files.pythonhosted.org/packages/19/fa/0172e350361d61febcea941b0cc541d6e6c8d65d153e85f850a7b256ff8a/xxhash-3.6.0-cp313-cp313t-win32.whl", hash = "sha256:1244460adc3a9be84731d72b8e80625788e5815b68da3da8b83f78115a40a7ec", size = 30916, upload-time = "2025-10-02T14:35:35.107Z" }, + { url = "https://files.pythonhosted.org/packages/ad/e6/e8cf858a2b19d6d45820f072eff1bea413910592ff17157cabc5f1227a16/xxhash-3.6.0-cp313-cp313t-win_amd64.whl", hash = "sha256:b1e420ef35c503869c4064f4a2f2b08ad6431ab7b229a05cce39d74268bca6b8", size = 31799, upload-time = "2025-10-02T14:35:36.165Z" }, + { url = "https://files.pythonhosted.org/packages/56/15/064b197e855bfb7b343210e82490ae672f8bc7cdf3ddb02e92f64304ee8a/xxhash-3.6.0-cp313-cp313t-win_arm64.whl", hash = "sha256:ec44b73a4220623235f67a996c862049f375df3b1052d9899f40a6382c32d746", size = 28044, upload-time = "2025-10-02T14:35:37.195Z" }, + { url = "https://files.pythonhosted.org/packages/7e/5e/0138bc4484ea9b897864d59fce9be9086030825bc778b76cb5a33a906d37/xxhash-3.6.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a40a3d35b204b7cc7643cbcf8c9976d818cb47befcfac8bbefec8038ac363f3e", size = 32754, upload-time = "2025-10-02T14:35:38.245Z" }, + { url = "https://files.pythonhosted.org/packages/18/d7/5dac2eb2ec75fd771957a13e5dda560efb2176d5203f39502a5fc571f899/xxhash-3.6.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a54844be970d3fc22630b32d515e79a90d0a3ddb2644d8d7402e3c4c8da61405", size = 30846, upload-time = "2025-10-02T14:35:39.6Z" }, + { url = "https://files.pythonhosted.org/packages/fe/71/8bc5be2bb00deb5682e92e8da955ebe5fa982da13a69da5a40a4c8db12fb/xxhash-3.6.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:016e9190af8f0a4e3741343777710e3d5717427f175adfdc3e72508f59e2a7f3", size = 194343, upload-time = "2025-10-02T14:35:40.69Z" }, + { url = "https://files.pythonhosted.org/packages/e7/3b/52badfb2aecec2c377ddf1ae75f55db3ba2d321c5e164f14461c90837ef3/xxhash-3.6.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4f6f72232f849eb9d0141e2ebe2677ece15adfd0fa599bc058aad83c714bb2c6", size = 213074, upload-time = "2025-10-02T14:35:42.29Z" }, + { url = "https://files.pythonhosted.org/packages/a2/2b/ae46b4e9b92e537fa30d03dbc19cdae57ed407e9c26d163895e968e3de85/xxhash-3.6.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:63275a8aba7865e44b1813d2177e0f5ea7eadad3dd063a21f7cf9afdc7054063", size = 212388, upload-time = "2025-10-02T14:35:43.929Z" }, + { url = "https://files.pythonhosted.org/packages/f5/80/49f88d3afc724b4ac7fbd664c8452d6db51b49915be48c6982659e0e7942/xxhash-3.6.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cd01fa2aa00d8b017c97eb46b9a794fbdca53fc14f845f5a328c71254b0abb7", size = 445614, upload-time = "2025-10-02T14:35:45.216Z" }, + { url = "https://files.pythonhosted.org/packages/ed/ba/603ce3961e339413543d8cd44f21f2c80e2a7c5cfe692a7b1f2cccf58f3c/xxhash-3.6.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0226aa89035b62b6a86d3c68df4d7c1f47a342b8683da2b60cedcddb46c4d95b", size = 194024, upload-time = "2025-10-02T14:35:46.959Z" }, + { url = "https://files.pythonhosted.org/packages/78/d1/8e225ff7113bf81545cfdcd79eef124a7b7064a0bba53605ff39590b95c2/xxhash-3.6.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c6e193e9f56e4ca4923c61238cdaced324f0feac782544eb4c6d55ad5cc99ddd", size = 210541, upload-time = "2025-10-02T14:35:48.301Z" }, + { url = "https://files.pythonhosted.org/packages/6f/58/0f89d149f0bad89def1a8dd38feb50ccdeb643d9797ec84707091d4cb494/xxhash-3.6.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9176dcaddf4ca963d4deb93866d739a343c01c969231dbe21680e13a5d1a5bf0", size = 198305, upload-time = "2025-10-02T14:35:49.584Z" }, + { url = "https://files.pythonhosted.org/packages/11/38/5eab81580703c4df93feb5f32ff8fa7fe1e2c51c1f183ee4e48d4bb9d3d7/xxhash-3.6.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c1ce4009c97a752e682b897aa99aef84191077a9433eb237774689f14f8ec152", size = 210848, upload-time = "2025-10-02T14:35:50.877Z" }, + { url = "https://files.pythonhosted.org/packages/5e/6b/953dc4b05c3ce678abca756416e4c130d2382f877a9c30a20d08ee6a77c0/xxhash-3.6.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:8cb2f4f679b01513b7adbb9b1b2f0f9cdc31b70007eaf9d59d0878809f385b11", size = 414142, upload-time = "2025-10-02T14:35:52.15Z" }, + { url = "https://files.pythonhosted.org/packages/08/a9/238ec0d4e81a10eb5026d4a6972677cbc898ba6c8b9dbaec12ae001b1b35/xxhash-3.6.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:653a91d7c2ab54a92c19ccf43508b6a555440b9be1bc8be553376778be7f20b5", size = 191547, upload-time = "2025-10-02T14:35:53.547Z" }, + { url = "https://files.pythonhosted.org/packages/f1/ee/3cf8589e06c2164ac77c3bf0aa127012801128f1feebf2a079272da5737c/xxhash-3.6.0-cp314-cp314-win32.whl", hash = "sha256:a756fe893389483ee8c394d06b5ab765d96e68fbbfe6fde7aa17e11f5720559f", size = 31214, upload-time = "2025-10-02T14:35:54.746Z" }, + { url = "https://files.pythonhosted.org/packages/02/5d/a19552fbc6ad4cb54ff953c3908bbc095f4a921bc569433d791f755186f1/xxhash-3.6.0-cp314-cp314-win_amd64.whl", hash = "sha256:39be8e4e142550ef69629c9cd71b88c90e9a5db703fecbcf265546d9536ca4ad", size = 32290, upload-time = "2025-10-02T14:35:55.791Z" }, + { url = "https://files.pythonhosted.org/packages/b1/11/dafa0643bc30442c887b55baf8e73353a344ee89c1901b5a5c54a6c17d39/xxhash-3.6.0-cp314-cp314-win_arm64.whl", hash = "sha256:25915e6000338999236f1eb68a02a32c3275ac338628a7eaa5a269c401995679", size = 28795, upload-time = "2025-10-02T14:35:57.162Z" }, + { url = "https://files.pythonhosted.org/packages/2c/db/0e99732ed7f64182aef4a6fb145e1a295558deec2a746265dcdec12d191e/xxhash-3.6.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c5294f596a9017ca5a3e3f8884c00b91ab2ad2933cf288f4923c3fd4346cf3d4", size = 32955, upload-time = "2025-10-02T14:35:58.267Z" }, + { url = "https://files.pythonhosted.org/packages/55/f4/2a7c3c68e564a099becfa44bb3d398810cc0ff6749b0d3cb8ccb93f23c14/xxhash-3.6.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1cf9dcc4ab9cff01dfbba78544297a3a01dafd60f3bde4e2bfd016cf7e4ddc67", size = 31072, upload-time = "2025-10-02T14:35:59.382Z" }, + { url = "https://files.pythonhosted.org/packages/c6/d9/72a29cddc7250e8a5819dad5d466facb5dc4c802ce120645630149127e73/xxhash-3.6.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:01262da8798422d0685f7cef03b2bd3f4f46511b02830861df548d7def4402ad", size = 196579, upload-time = "2025-10-02T14:36:00.838Z" }, + { url = "https://files.pythonhosted.org/packages/63/93/b21590e1e381040e2ca305a884d89e1c345b347404f7780f07f2cdd47ef4/xxhash-3.6.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51a73fb7cb3a3ead9f7a8b583ffd9b8038e277cdb8cb87cf890e88b3456afa0b", size = 215854, upload-time = "2025-10-02T14:36:02.207Z" }, + { url = "https://files.pythonhosted.org/packages/ce/b8/edab8a7d4fa14e924b29be877d54155dcbd8b80be85ea00d2be3413a9ed4/xxhash-3.6.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b9c6df83594f7df8f7f708ce5ebeacfc69f72c9fbaaababf6cf4758eaada0c9b", size = 214965, upload-time = "2025-10-02T14:36:03.507Z" }, + { url = "https://files.pythonhosted.org/packages/27/67/dfa980ac7f0d509d54ea0d5a486d2bb4b80c3f1bb22b66e6a05d3efaf6c0/xxhash-3.6.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:627f0af069b0ea56f312fd5189001c24578868643203bca1abbc2c52d3a6f3ca", size = 448484, upload-time = "2025-10-02T14:36:04.828Z" }, + { url = "https://files.pythonhosted.org/packages/8c/63/8ffc2cc97e811c0ca5d00ab36604b3ea6f4254f20b7bc658ca825ce6c954/xxhash-3.6.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aa912c62f842dfd013c5f21a642c9c10cd9f4c4e943e0af83618b4a404d9091a", size = 196162, upload-time = "2025-10-02T14:36:06.182Z" }, + { url = "https://files.pythonhosted.org/packages/4b/77/07f0e7a3edd11a6097e990f6e5b815b6592459cb16dae990d967693e6ea9/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b465afd7909db30168ab62afe40b2fcf79eedc0b89a6c0ab3123515dc0df8b99", size = 213007, upload-time = "2025-10-02T14:36:07.733Z" }, + { url = "https://files.pythonhosted.org/packages/ae/d8/bc5fa0d152837117eb0bef6f83f956c509332ce133c91c63ce07ee7c4873/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:a881851cf38b0a70e7c4d3ce81fc7afd86fbc2a024f4cfb2a97cf49ce04b75d3", size = 200956, upload-time = "2025-10-02T14:36:09.106Z" }, + { url = "https://files.pythonhosted.org/packages/26/a5/d749334130de9411783873e9b98ecc46688dad5db64ca6e04b02acc8b473/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:9b3222c686a919a0f3253cfc12bb118b8b103506612253b5baeaac10d8027cf6", size = 213401, upload-time = "2025-10-02T14:36:10.585Z" }, + { url = "https://files.pythonhosted.org/packages/89/72/abed959c956a4bfc72b58c0384bb7940663c678127538634d896b1195c10/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:c5aa639bc113e9286137cec8fadc20e9cd732b2cc385c0b7fa673b84fc1f2a93", size = 417083, upload-time = "2025-10-02T14:36:12.276Z" }, + { url = "https://files.pythonhosted.org/packages/0c/b3/62fd2b586283b7d7d665fb98e266decadf31f058f1cf6c478741f68af0cb/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5c1343d49ac102799905e115aee590183c3921d475356cb24b4de29a4bc56518", size = 193913, upload-time = "2025-10-02T14:36:14.025Z" }, + { url = "https://files.pythonhosted.org/packages/9a/9a/c19c42c5b3f5a4aad748a6d5b4f23df3bed7ee5445accc65a0fb3ff03953/xxhash-3.6.0-cp314-cp314t-win32.whl", hash = "sha256:5851f033c3030dd95c086b4a36a2683c2ff4a799b23af60977188b057e467119", size = 31586, upload-time = "2025-10-02T14:36:15.603Z" }, + { url = "https://files.pythonhosted.org/packages/03/d6/4cc450345be9924fd5dc8c590ceda1db5b43a0a889587b0ae81a95511360/xxhash-3.6.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0444e7967dac37569052d2409b00a8860c2135cff05502df4da80267d384849f", size = 32526, upload-time = "2025-10-02T14:36:16.708Z" }, + { url = "https://files.pythonhosted.org/packages/0f/c9/7243eb3f9eaabd1a88a5a5acadf06df2d83b100c62684b7425c6a11bcaa8/xxhash-3.6.0-cp314-cp314t-win_arm64.whl", hash = "sha256:bb79b1e63f6fd84ec778a4b1916dfe0a7c3fdb986c06addd5db3a0d413819d95", size = 28898, upload-time = "2025-10-02T14:36:17.843Z" }, + { url = "https://files.pythonhosted.org/packages/93/1e/8aec23647a34a249f62e2398c42955acd9b4c6ed5cf08cbea94dc46f78d2/xxhash-3.6.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0f7b7e2ec26c1666ad5fc9dbfa426a6a3367ceaf79db5dd76264659d509d73b0", size = 30662, upload-time = "2025-10-02T14:37:01.743Z" }, + { url = "https://files.pythonhosted.org/packages/b8/0b/b14510b38ba91caf43006209db846a696ceea6a847a0c9ba0a5b1adc53d6/xxhash-3.6.0-pp311-pypy311_pp73-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5dc1e14d14fa0f5789ec29a7062004b5933964bb9b02aae6622b8f530dc40296", size = 41056, upload-time = "2025-10-02T14:37:02.879Z" }, + { url = "https://files.pythonhosted.org/packages/50/55/15a7b8a56590e66ccd374bbfa3f9ffc45b810886c8c3b614e3f90bd2367c/xxhash-3.6.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:881b47fc47e051b37d94d13e7455131054b56749b91b508b0907eb07900d1c13", size = 36251, upload-time = "2025-10-02T14:37:04.44Z" }, + { url = "https://files.pythonhosted.org/packages/62/b2/5ac99a041a29e58e95f907876b04f7067a0242cb85b5f39e726153981503/xxhash-3.6.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c6dc31591899f5e5666f04cc2e529e69b4072827085c1ef15294d91a004bc1bd", size = 32481, upload-time = "2025-10-02T14:37:05.869Z" }, + { url = "https://files.pythonhosted.org/packages/7b/d9/8d95e906764a386a3d3b596f3c68bb63687dfca806373509f51ce8eea81f/xxhash-3.6.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:15e0dac10eb9309508bfc41f7f9deaa7755c69e35af835db9cb10751adebc35d", size = 31565, upload-time = "2025-10-02T14:37:06.966Z" }, +] diff --git a/netrun/pyproject.toml b/netrun/pyproject.toml index 779e4649..9ffaf5e9 100644 --- a/netrun/pyproject.toml +++ b/netrun/pyproject.toml @@ -15,12 +15,8 @@ dependencies = [ [project.optional-dependencies] deploy = ["pyinfra>=3.0"] -cli = ["typer>=0.15.0", "tomli-w>=1.0"] serialization = ["dill>=0.3", "cloudpickle>=3.0"] -[project.scripts] -netrun = "netrun.cli._app:app_main" - [project.urls] Homepage = "https://github.com/lukastk/netrun" Documentation = "https://github.com/lukastk/netrun" @@ -32,7 +28,7 @@ dev = [ "jupyterlab>=4.5.1", "mypy>=1.19.1", "nblite>=1.1.5", - "netrun[cli]", + "netrun-cli", "pytest>=9.0.2", "pytest-asyncio>=1.3.0", "ruff>=0.8.0", @@ -41,6 +37,7 @@ dev = [ [tool.uv.sources] dev = { path = "src/dev", editable = true } netrun-sim = { path = "../netrun-sim/python", editable = true } +netrun-cli = { path = "../netrun-cli", editable = true } [build-system] requires = ["hatchling"] diff --git a/netrun/uv.lock b/netrun/uv.lock index f8afab66..5ff268e1 100644 --- a/netrun/uv.lock +++ b/netrun/uv.lock @@ -3,9 +3,14 @@ revision = 3 requires-python = ">=3.11" resolution-markers = [ "python_full_version >= '3.14'", - "python_full_version < '3.14'", + "python_full_version >= '3.12' and python_full_version < '3.14'", + "python_full_version < '3.12'", ] +[options] +exclude-newer = "2026-04-21T10:17:58.073617Z" +exclude-newer-span = "P7D" + [[package]] name = "annotated-doc" version = "0.0.4" @@ -26,15 +31,15 @@ wheels = [ [[package]] name = "anyio" -version = "4.12.1" +version = "4.13.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "idna" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" } +sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, + { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" }, ] [[package]] @@ -113,20 +118,20 @@ wheels = [ [[package]] name = "async-lru" -version = "2.2.0" +version = "2.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/05/8a/ca724066c32a53fa75f59e0f21aa822fdaa8a0dffa112d223634e3caabf9/async_lru-2.2.0.tar.gz", hash = "sha256:80abae2a237dbc6c60861d621619af39f0d920aea306de34cb992c879e01370c", size = 14654, upload-time = "2026-02-20T19:11:43.848Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/1f/989ecfef8e64109a489fff357450cb73fa73a865a92bd8c272170a6922c2/async_lru-2.3.0.tar.gz", hash = "sha256:89bdb258a0140d7313cf8f4031d816a042202faa61d0ab310a0a538baa1c24b6", size = 16332, upload-time = "2026-03-19T01:04:32.413Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/13/5c/af990f019b8dd11c5492a6371fe74a5b0276357370030b67254a87329944/async_lru-2.2.0-py3-none-any.whl", hash = "sha256:e2c1cf731eba202b59c5feedaef14ffd9d02ad0037fcda64938699f2c380eafe", size = 7890, upload-time = "2026-02-20T19:11:42.273Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e2/c2e3abf398f80732e58b03be77bde9022550d221dd8781bf586bd4d97cc1/async_lru-2.3.0-py3-none-any.whl", hash = "sha256:eea27b01841909316f2cc739807acea1c623df2be8c5cfad7583286397bb8315", size = 8403, upload-time = "2026-03-19T01:04:30.883Z" }, ] [[package]] name = "attrs" -version = "25.4.0" +version = "26.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, + { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, ] [[package]] @@ -328,87 +333,103 @@ wheels = [ [[package]] name = "charset-normalizer" -version = "3.4.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" }, - { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" }, - { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" }, - { url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" }, - { url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" }, - { url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" }, - { url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" }, - { url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" }, - { url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" }, - { url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" }, - { url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" }, - { url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" }, - { url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" }, - { url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" }, - { url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" }, - { url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" }, - { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" }, - { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" }, - { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" }, - { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" }, - { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" }, - { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" }, - { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" }, - { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" }, - { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" }, - { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" }, - { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" }, - { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" }, - { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" }, - { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" }, - { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" }, - { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" }, - { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" }, - { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" }, - { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" }, - { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" }, - { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" }, - { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" }, - { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" }, - { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" }, - { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" }, - { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" }, - { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" }, - { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" }, - { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" }, - { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" }, - { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" }, - { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" }, - { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, - { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, - { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, - { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" }, - { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" }, - { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" }, - { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" }, - { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" }, - { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" }, - { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" }, - { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" }, - { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" }, - { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" }, - { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, - { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, - { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, - { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, +version = "3.4.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/d7/b5b7020a0565c2e9fa8c09f4b5fa6232feb326b8c20081ccded47ea368fd/charset_normalizer-3.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7641bb8895e77f921102f72833904dcd9901df5d6d72a2ab8f31d04b7e51e4e7", size = 309705, upload-time = "2026-04-02T09:26:02.191Z" }, + { url = "https://files.pythonhosted.org/packages/5a/53/58c29116c340e5456724ecd2fff4196d236b98f3da97b404bc5e51ac3493/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:202389074300232baeb53ae2569a60901f7efadd4245cf3a3bf0617d60b439d7", size = 206419, upload-time = "2026-04-02T09:26:03.583Z" }, + { url = "https://files.pythonhosted.org/packages/b2/02/e8146dc6591a37a00e5144c63f29fb7c97a734ea8a111190783c0e60ab63/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:30b8d1d8c52a48c2c5690e152c169b673487a2a58de1ec7393196753063fcd5e", size = 227901, upload-time = "2026-04-02T09:26:04.738Z" }, + { url = "https://files.pythonhosted.org/packages/fb/73/77486c4cd58f1267bf17db420e930c9afa1b3be3fe8c8b8ebbebc9624359/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:532bc9bf33a68613fd7d65e4b1c71a6a38d7d42604ecf239c77392e9b4e8998c", size = 222742, upload-time = "2026-04-02T09:26:06.36Z" }, + { url = "https://files.pythonhosted.org/packages/a1/fa/f74eb381a7d94ded44739e9d94de18dc5edc9c17fb8c11f0a6890696c0a9/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fe249cb4651fd12605b7288b24751d8bfd46d35f12a20b1ba33dea122e690df", size = 214061, upload-time = "2026-04-02T09:26:08.347Z" }, + { url = "https://files.pythonhosted.org/packages/dc/92/42bd3cefcf7687253fb86694b45f37b733c97f59af3724f356fa92b8c344/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:65bcd23054beab4d166035cabbc868a09c1a49d1efe458fe8e4361215df40265", size = 199239, upload-time = "2026-04-02T09:26:09.823Z" }, + { url = "https://files.pythonhosted.org/packages/4c/3d/069e7184e2aa3b3cddc700e3dd267413dc259854adc3380421c805c6a17d/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:08e721811161356f97b4059a9ba7bafb23ea5ee2255402c42881c214e173c6b4", size = 210173, upload-time = "2026-04-02T09:26:10.953Z" }, + { url = "https://files.pythonhosted.org/packages/62/51/9d56feb5f2e7074c46f93e0ebdbe61f0848ee246e2f0d89f8e20b89ebb8f/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e060d01aec0a910bdccb8be71faf34e7799ce36950f8294c8bf612cba65a2c9e", size = 209841, upload-time = "2026-04-02T09:26:12.142Z" }, + { url = "https://files.pythonhosted.org/packages/d2/59/893d8f99cc4c837dda1fe2f1139079703deb9f321aabcb032355de13b6c7/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:38c0109396c4cfc574d502df99742a45c72c08eff0a36158b6f04000043dbf38", size = 200304, upload-time = "2026-04-02T09:26:13.711Z" }, + { url = "https://files.pythonhosted.org/packages/7d/1d/ee6f3be3464247578d1ed5c46de545ccc3d3ff933695395c402c21fa6b77/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1c2a768fdd44ee4a9339a9b0b130049139b8ce3c01d2ce09f67f5a68048d477c", size = 229455, upload-time = "2026-04-02T09:26:14.941Z" }, + { url = "https://files.pythonhosted.org/packages/54/bb/8fb0a946296ea96a488928bdce8ef99023998c48e4713af533e9bb98ef07/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:1a87ca9d5df6fe460483d9a5bbf2b18f620cbed41b432e2bddb686228282d10b", size = 210036, upload-time = "2026-04-02T09:26:16.478Z" }, + { url = "https://files.pythonhosted.org/packages/9a/bc/015b2387f913749f82afd4fcba07846d05b6d784dd16123cb66860e0237d/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d635aab80466bc95771bb78d5370e74d36d1fe31467b6b29b8b57b2a3cd7d22c", size = 224739, upload-time = "2026-04-02T09:26:17.751Z" }, + { url = "https://files.pythonhosted.org/packages/17/ab/63133691f56baae417493cba6b7c641571a2130eb7bceba6773367ab9ec5/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ae196f021b5e7c78e918242d217db021ed2a6ace2bc6ae94c0fc596221c7f58d", size = 216277, upload-time = "2026-04-02T09:26:18.981Z" }, + { url = "https://files.pythonhosted.org/packages/06/6d/3be70e827977f20db77c12a97e6a9f973631a45b8d186c084527e53e77a4/charset_normalizer-3.4.7-cp311-cp311-win32.whl", hash = "sha256:adb2597b428735679446b46c8badf467b4ca5f5056aae4d51a19f9570301b1ad", size = 147819, upload-time = "2026-04-02T09:26:20.295Z" }, + { url = "https://files.pythonhosted.org/packages/20/d9/5f67790f06b735d7c7637171bbfd89882ad67201891b7275e51116ed8207/charset_normalizer-3.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:8e385e4267ab76874ae30db04c627faaaf0b509e1ccc11a95b3fc3e83f855c00", size = 159281, upload-time = "2026-04-02T09:26:21.74Z" }, + { url = "https://files.pythonhosted.org/packages/ca/83/6413f36c5a34afead88ce6f66684d943d91f233d76dd083798f9602b75ae/charset_normalizer-3.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:d4a48e5b3c2a489fae013b7589308a40146ee081f6f509e047e0e096084ceca1", size = 147843, upload-time = "2026-04-02T09:26:22.901Z" }, + { url = "https://files.pythonhosted.org/packages/0c/eb/4fc8d0a7110eb5fc9cc161723a34a8a6c200ce3b4fbf681bc86feee22308/charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46", size = 311328, upload-time = "2026-04-02T09:26:24.331Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2", size = 208061, upload-time = "2026-04-02T09:26:25.568Z" }, + { url = "https://files.pythonhosted.org/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b", size = 229031, upload-time = "2026-04-02T09:26:26.865Z" }, + { url = "https://files.pythonhosted.org/packages/dc/67/675a46eb016118a2fbde5a277a5d15f4f69d5f3f5f338e5ee2f8948fcf43/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a", size = 225239, upload-time = "2026-04-02T09:26:28.044Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f8/d0118a2f5f23b02cd166fa385c60f9b0d4f9194f574e2b31cef350ad7223/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116", size = 216589, upload-time = "2026-04-02T09:26:29.239Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f1/6d2b0b261b6c4ceef0fcb0d17a01cc5bc53586c2d4796fa04b5c540bc13d/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb", size = 202733, upload-time = "2026-04-02T09:26:30.5Z" }, + { url = "https://files.pythonhosted.org/packages/6f/c0/7b1f943f7e87cc3db9626ba17807d042c38645f0a1d4415c7a14afb5591f/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1", size = 212652, upload-time = "2026-04-02T09:26:31.709Z" }, + { url = "https://files.pythonhosted.org/packages/38/dd/5a9ab159fe45c6e72079398f277b7d2b523e7f716acc489726115a910097/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15", size = 211229, upload-time = "2026-04-02T09:26:33.282Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ff/531a1cad5ca855d1c1a8b69cb71abfd6d85c0291580146fda7c82857caa1/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5", size = 203552, upload-time = "2026-04-02T09:26:34.845Z" }, + { url = "https://files.pythonhosted.org/packages/c1/4c/a5fb52d528a8ca41f7598cb619409ece30a169fbdf9cdce592e53b46c3a6/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d", size = 230806, upload-time = "2026-04-02T09:26:36.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/7a/071feed8124111a32b316b33ae4de83d36923039ef8cf48120266844285b/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7", size = 212316, upload-time = "2026-04-02T09:26:37.672Z" }, + { url = "https://files.pythonhosted.org/packages/fd/35/f7dba3994312d7ba508e041eaac39a36b120f32d4c8662b8814dab876431/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464", size = 227274, upload-time = "2026-04-02T09:26:38.93Z" }, + { url = "https://files.pythonhosted.org/packages/8a/2d/a572df5c9204ab7688ec1edc895a73ebded3b023bb07364710b05dd1c9be/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49", size = 218468, upload-time = "2026-04-02T09:26:40.17Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/890922a8b03a568ca2f336c36585a4713c55d4d67bf0f0c78924be6315ca/charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c", size = 148460, upload-time = "2026-04-02T09:26:41.416Z" }, + { url = "https://files.pythonhosted.org/packages/35/d9/0e7dffa06c5ab081f75b1b786f0aefc88365825dfcd0ac544bdb7b2b6853/charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6", size = 159330, upload-time = "2026-04-02T09:26:42.554Z" }, + { url = "https://files.pythonhosted.org/packages/9e/5d/481bcc2a7c88ea6b0878c299547843b2521ccbc40980cb406267088bc701/charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d", size = 147828, upload-time = "2026-04-02T09:26:44.075Z" }, + { url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" }, + { url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" }, + { url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" }, + { url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" }, + { url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" }, + { url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" }, + { url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" }, + { url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" }, + { url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" }, + { url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" }, + { url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" }, + { url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" }, + { url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" }, + { url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" }, + { url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" }, + { url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" }, + { url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" }, + { url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" }, + { url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" }, + { url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" }, + { url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" }, + { url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" }, + { url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" }, + { url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" }, + { url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" }, + { url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" }, + { url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" }, + { url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" }, + { url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" }, + { url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" }, + { url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" }, + { url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" }, + { url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" }, + { url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" }, + { url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" }, + { url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, ] [[package]] name = "click" -version = "8.3.1" +version = "8.3.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +sdist = { url = "https://files.pythonhosted.org/packages/57/75/31212c6bf2503fdf920d87fee5d7a86a2e3bcf444984126f13d8e4016804/click-8.3.2.tar.gz", hash = "sha256:14162b8b3b3550a7d479eafa77dfd3c38d9dc8951f6f69c78913a8f9a7540fd5", size = 302856, upload-time = "2026-04-03T19:14:45.118Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, + { url = "https://files.pythonhosted.org/packages/e4/20/71885d8b97d4f3dde17b1fdb92dbd4908b00541c5a3379787137285f602e/click-8.3.2-py3-none-any.whl", hash = "sha256:1924d2c27c5653561cd2cae4548d1406039cb79b858b747cfea24924bbc1616d", size = 108379, upload-time = "2026-04-03T19:14:43.505Z" }, ] [[package]] @@ -440,61 +461,61 @@ wheels = [ [[package]] name = "cryptography" -version = "46.0.5" +version = "46.0.7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/60/04/ee2a9e8542e4fa2773b81771ff8349ff19cdd56b7258a0cc442639052edb/cryptography-46.0.5.tar.gz", hash = "sha256:abace499247268e3757271b2f1e244b36b06f8515cf27c4d49468fc9eb16e93d", size = 750064, upload-time = "2026-02-10T19:18:38.255Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/81/b0bb27f2ba931a65409c6b8a8b358a7f03c0e46eceacddff55f7c84b1f3b/cryptography-46.0.5-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:351695ada9ea9618b3500b490ad54c739860883df6c1f555e088eaf25b1bbaad", size = 7176289, upload-time = "2026-02-10T19:17:08.274Z" }, - { url = "https://files.pythonhosted.org/packages/ff/9e/6b4397a3e3d15123de3b1806ef342522393d50736c13b20ec4c9ea6693a6/cryptography-46.0.5-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c18ff11e86df2e28854939acde2d003f7984f721eba450b56a200ad90eeb0e6b", size = 4275637, upload-time = "2026-02-10T19:17:10.53Z" }, - { url = "https://files.pythonhosted.org/packages/63/e7/471ab61099a3920b0c77852ea3f0ea611c9702f651600397ac567848b897/cryptography-46.0.5-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d7e3d356b8cd4ea5aff04f129d5f66ebdc7b6f8eae802b93739ed520c47c79b", size = 4424742, upload-time = "2026-02-10T19:17:12.388Z" }, - { url = "https://files.pythonhosted.org/packages/37/53/a18500f270342d66bf7e4d9f091114e31e5ee9e7375a5aba2e85a91e0044/cryptography-46.0.5-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:50bfb6925eff619c9c023b967d5b77a54e04256c4281b0e21336a130cd7fc263", size = 4277528, upload-time = "2026-02-10T19:17:13.853Z" }, - { url = "https://files.pythonhosted.org/packages/22/29/c2e812ebc38c57b40e7c583895e73c8c5adb4d1e4a0cc4c5a4fdab2b1acc/cryptography-46.0.5-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:803812e111e75d1aa73690d2facc295eaefd4439be1023fefc4995eaea2af90d", size = 4947993, upload-time = "2026-02-10T19:17:15.618Z" }, - { url = "https://files.pythonhosted.org/packages/6b/e7/237155ae19a9023de7e30ec64e5d99a9431a567407ac21170a046d22a5a3/cryptography-46.0.5-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ee190460e2fbe447175cda91b88b84ae8322a104fc27766ad09428754a618ed", size = 4456855, upload-time = "2026-02-10T19:17:17.221Z" }, - { url = "https://files.pythonhosted.org/packages/2d/87/fc628a7ad85b81206738abbd213b07702bcbdada1dd43f72236ef3cffbb5/cryptography-46.0.5-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:f145bba11b878005c496e93e257c1e88f154d278d2638e6450d17e0f31e558d2", size = 3984635, upload-time = "2026-02-10T19:17:18.792Z" }, - { url = "https://files.pythonhosted.org/packages/84/29/65b55622bde135aedf4565dc509d99b560ee4095e56989e815f8fd2aa910/cryptography-46.0.5-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e9251e3be159d1020c4030bd2e5f84d6a43fe54b6c19c12f51cde9542a2817b2", size = 4277038, upload-time = "2026-02-10T19:17:20.256Z" }, - { url = "https://files.pythonhosted.org/packages/bc/36/45e76c68d7311432741faf1fbf7fac8a196a0a735ca21f504c75d37e2558/cryptography-46.0.5-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:47fb8a66058b80e509c47118ef8a75d14c455e81ac369050f20ba0d23e77fee0", size = 4912181, upload-time = "2026-02-10T19:17:21.825Z" }, - { url = "https://files.pythonhosted.org/packages/6d/1a/c1ba8fead184d6e3d5afcf03d569acac5ad063f3ac9fb7258af158f7e378/cryptography-46.0.5-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:4c3341037c136030cb46e4b1e17b7418ea4cbd9dd207e4a6f3b2b24e0d4ac731", size = 4456482, upload-time = "2026-02-10T19:17:25.133Z" }, - { url = "https://files.pythonhosted.org/packages/f9/e5/3fb22e37f66827ced3b902cf895e6a6bc1d095b5b26be26bd13c441fdf19/cryptography-46.0.5-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:890bcb4abd5a2d3f852196437129eb3667d62630333aacc13dfd470fad3aaa82", size = 4405497, upload-time = "2026-02-10T19:17:26.66Z" }, - { url = "https://files.pythonhosted.org/packages/1a/df/9d58bb32b1121a8a2f27383fabae4d63080c7ca60b9b5c88be742be04ee7/cryptography-46.0.5-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:80a8d7bfdf38f87ca30a5391c0c9ce4ed2926918e017c29ddf643d0ed2778ea1", size = 4667819, upload-time = "2026-02-10T19:17:28.569Z" }, - { url = "https://files.pythonhosted.org/packages/ea/ed/325d2a490c5e94038cdb0117da9397ece1f11201f425c4e9c57fe5b9f08b/cryptography-46.0.5-cp311-abi3-win32.whl", hash = "sha256:60ee7e19e95104d4c03871d7d7dfb3d22ef8a9b9c6778c94e1c8fcc8365afd48", size = 3028230, upload-time = "2026-02-10T19:17:30.518Z" }, - { url = "https://files.pythonhosted.org/packages/e9/5a/ac0f49e48063ab4255d9e3b79f5def51697fce1a95ea1370f03dc9db76f6/cryptography-46.0.5-cp311-abi3-win_amd64.whl", hash = "sha256:38946c54b16c885c72c4f59846be9743d699eee2b69b6988e0a00a01f46a61a4", size = 3480909, upload-time = "2026-02-10T19:17:32.083Z" }, - { url = "https://files.pythonhosted.org/packages/00/13/3d278bfa7a15a96b9dc22db5a12ad1e48a9eb3d40e1827ef66a5df75d0d0/cryptography-46.0.5-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:94a76daa32eb78d61339aff7952ea819b1734b46f73646a07decb40e5b3448e2", size = 7119287, upload-time = "2026-02-10T19:17:33.801Z" }, - { url = "https://files.pythonhosted.org/packages/67/c8/581a6702e14f0898a0848105cbefd20c058099e2c2d22ef4e476dfec75d7/cryptography-46.0.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5be7bf2fb40769e05739dd0046e7b26f9d4670badc7b032d6ce4db64dddc0678", size = 4265728, upload-time = "2026-02-10T19:17:35.569Z" }, - { url = "https://files.pythonhosted.org/packages/dd/4a/ba1a65ce8fc65435e5a849558379896c957870dd64fecea97b1ad5f46a37/cryptography-46.0.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe346b143ff9685e40192a4960938545c699054ba11d4f9029f94751e3f71d87", size = 4408287, upload-time = "2026-02-10T19:17:36.938Z" }, - { url = "https://files.pythonhosted.org/packages/f8/67/8ffdbf7b65ed1ac224d1c2df3943553766914a8ca718747ee3871da6107e/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:c69fd885df7d089548a42d5ec05be26050ebcd2283d89b3d30676eb32ff87dee", size = 4270291, upload-time = "2026-02-10T19:17:38.748Z" }, - { url = "https://files.pythonhosted.org/packages/f8/e5/f52377ee93bc2f2bba55a41a886fd208c15276ffbd2569f2ddc89d50e2c5/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:8293f3dea7fc929ef7240796ba231413afa7b68ce38fd21da2995549f5961981", size = 4927539, upload-time = "2026-02-10T19:17:40.241Z" }, - { url = "https://files.pythonhosted.org/packages/3b/02/cfe39181b02419bbbbcf3abdd16c1c5c8541f03ca8bda240debc467d5a12/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:1abfdb89b41c3be0365328a410baa9df3ff8a9110fb75e7b52e66803ddabc9a9", size = 4442199, upload-time = "2026-02-10T19:17:41.789Z" }, - { url = "https://files.pythonhosted.org/packages/c0/96/2fcaeb4873e536cf71421a388a6c11b5bc846e986b2b069c79363dc1648e/cryptography-46.0.5-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:d66e421495fdb797610a08f43b05269e0a5ea7f5e652a89bfd5a7d3c1dee3648", size = 3960131, upload-time = "2026-02-10T19:17:43.379Z" }, - { url = "https://files.pythonhosted.org/packages/d8/d2/b27631f401ddd644e94c5cf33c9a4069f72011821cf3dc7309546b0642a0/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:4e817a8920bfbcff8940ecfd60f23d01836408242b30f1a708d93198393a80b4", size = 4270072, upload-time = "2026-02-10T19:17:45.481Z" }, - { url = "https://files.pythonhosted.org/packages/f4/a7/60d32b0370dae0b4ebe55ffa10e8599a2a59935b5ece1b9f06edb73abdeb/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:68f68d13f2e1cb95163fa3b4db4bf9a159a418f5f6e7242564fc75fcae667fd0", size = 4892170, upload-time = "2026-02-10T19:17:46.997Z" }, - { url = "https://files.pythonhosted.org/packages/d2/b9/cf73ddf8ef1164330eb0b199a589103c363afa0cf794218c24d524a58eab/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:a3d1fae9863299076f05cb8a778c467578262fae09f9dc0ee9b12eb4268ce663", size = 4441741, upload-time = "2026-02-10T19:17:48.661Z" }, - { url = "https://files.pythonhosted.org/packages/5f/eb/eee00b28c84c726fe8fa0158c65afe312d9c3b78d9d01daf700f1f6e37ff/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c4143987a42a2397f2fc3b4d7e3a7d313fbe684f67ff443999e803dd75a76826", size = 4396728, upload-time = "2026-02-10T19:17:50.058Z" }, - { url = "https://files.pythonhosted.org/packages/65/f4/6bc1a9ed5aef7145045114b75b77c2a8261b4d38717bd8dea111a63c3442/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7d731d4b107030987fd61a7f8ab512b25b53cef8f233a97379ede116f30eb67d", size = 4652001, upload-time = "2026-02-10T19:17:51.54Z" }, - { url = "https://files.pythonhosted.org/packages/86/ef/5d00ef966ddd71ac2e6951d278884a84a40ffbd88948ef0e294b214ae9e4/cryptography-46.0.5-cp314-cp314t-win32.whl", hash = "sha256:c3bcce8521d785d510b2aad26ae2c966092b7daa8f45dd8f44734a104dc0bc1a", size = 3003637, upload-time = "2026-02-10T19:17:52.997Z" }, - { url = "https://files.pythonhosted.org/packages/b7/57/f3f4160123da6d098db78350fdfd9705057aad21de7388eacb2401dceab9/cryptography-46.0.5-cp314-cp314t-win_amd64.whl", hash = "sha256:4d8ae8659ab18c65ced284993c2265910f6c9e650189d4e3f68445ef82a810e4", size = 3469487, upload-time = "2026-02-10T19:17:54.549Z" }, - { url = "https://files.pythonhosted.org/packages/e2/fa/a66aa722105ad6a458bebd64086ca2b72cdd361fed31763d20390f6f1389/cryptography-46.0.5-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:4108d4c09fbbf2789d0c926eb4152ae1760d5a2d97612b92d508d96c861e4d31", size = 7170514, upload-time = "2026-02-10T19:17:56.267Z" }, - { url = "https://files.pythonhosted.org/packages/0f/04/c85bdeab78c8bc77b701bf0d9bdcf514c044e18a46dcff330df5448631b0/cryptography-46.0.5-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1f30a86d2757199cb2d56e48cce14deddf1f9c95f1ef1b64ee91ea43fe2e18", size = 4275349, upload-time = "2026-02-10T19:17:58.419Z" }, - { url = "https://files.pythonhosted.org/packages/5c/32/9b87132a2f91ee7f5223b091dc963055503e9b442c98fc0b8a5ca765fab0/cryptography-46.0.5-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:039917b0dc418bb9f6edce8a906572d69e74bd330b0b3fea4f79dab7f8ddd235", size = 4420667, upload-time = "2026-02-10T19:18:00.619Z" }, - { url = "https://files.pythonhosted.org/packages/a1/a6/a7cb7010bec4b7c5692ca6f024150371b295ee1c108bdc1c400e4c44562b/cryptography-46.0.5-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ba2a27ff02f48193fc4daeadf8ad2590516fa3d0adeeb34336b96f7fa64c1e3a", size = 4276980, upload-time = "2026-02-10T19:18:02.379Z" }, - { url = "https://files.pythonhosted.org/packages/8e/7c/c4f45e0eeff9b91e3f12dbd0e165fcf2a38847288fcfd889deea99fb7b6d/cryptography-46.0.5-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:61aa400dce22cb001a98014f647dc21cda08f7915ceb95df0c9eaf84b4b6af76", size = 4939143, upload-time = "2026-02-10T19:18:03.964Z" }, - { url = "https://files.pythonhosted.org/packages/37/19/e1b8f964a834eddb44fa1b9a9976f4e414cbb7aa62809b6760c8803d22d1/cryptography-46.0.5-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ce58ba46e1bc2aac4f7d9290223cead56743fa6ab94a5d53292ffaac6a91614", size = 4453674, upload-time = "2026-02-10T19:18:05.588Z" }, - { url = "https://files.pythonhosted.org/packages/db/ed/db15d3956f65264ca204625597c410d420e26530c4e2943e05a0d2f24d51/cryptography-46.0.5-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:420d0e909050490d04359e7fdb5ed7e667ca5c3c402b809ae2563d7e66a92229", size = 3978801, upload-time = "2026-02-10T19:18:07.167Z" }, - { url = "https://files.pythonhosted.org/packages/41/e2/df40a31d82df0a70a0daf69791f91dbb70e47644c58581d654879b382d11/cryptography-46.0.5-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:582f5fcd2afa31622f317f80426a027f30dc792e9c80ffee87b993200ea115f1", size = 4276755, upload-time = "2026-02-10T19:18:09.813Z" }, - { url = "https://files.pythonhosted.org/packages/33/45/726809d1176959f4a896b86907b98ff4391a8aa29c0aaaf9450a8a10630e/cryptography-46.0.5-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:bfd56bb4b37ed4f330b82402f6f435845a5f5648edf1ad497da51a8452d5d62d", size = 4901539, upload-time = "2026-02-10T19:18:11.263Z" }, - { url = "https://files.pythonhosted.org/packages/99/0f/a3076874e9c88ecb2ecc31382f6e7c21b428ede6f55aafa1aa272613e3cd/cryptography-46.0.5-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:a3d507bb6a513ca96ba84443226af944b0f7f47dcc9a399d110cd6146481d24c", size = 4452794, upload-time = "2026-02-10T19:18:12.914Z" }, - { url = "https://files.pythonhosted.org/packages/02/ef/ffeb542d3683d24194a38f66ca17c0a4b8bf10631feef44a7ef64e631b1a/cryptography-46.0.5-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9f16fbdf4da055efb21c22d81b89f155f02ba420558db21288b3d0035bafd5f4", size = 4404160, upload-time = "2026-02-10T19:18:14.375Z" }, - { url = "https://files.pythonhosted.org/packages/96/93/682d2b43c1d5f1406ed048f377c0fc9fc8f7b0447a478d5c65ab3d3a66eb/cryptography-46.0.5-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ced80795227d70549a411a4ab66e8ce307899fad2220ce5ab2f296e687eacde9", size = 4667123, upload-time = "2026-02-10T19:18:15.886Z" }, - { url = "https://files.pythonhosted.org/packages/45/2d/9c5f2926cb5300a8eefc3f4f0b3f3df39db7f7ce40c8365444c49363cbda/cryptography-46.0.5-cp38-abi3-win32.whl", hash = "sha256:02f547fce831f5096c9a567fd41bc12ca8f11df260959ecc7c3202555cc47a72", size = 3010220, upload-time = "2026-02-10T19:18:17.361Z" }, - { url = "https://files.pythonhosted.org/packages/48/ef/0c2f4a8e31018a986949d34a01115dd057bf536905dca38897bacd21fac3/cryptography-46.0.5-cp38-abi3-win_amd64.whl", hash = "sha256:556e106ee01aa13484ce9b0239bca667be5004efb0aabbed28d353df86445595", size = 3467050, upload-time = "2026-02-10T19:18:18.899Z" }, - { url = "https://files.pythonhosted.org/packages/eb/dd/2d9fdb07cebdf3d51179730afb7d5e576153c6744c3ff8fded23030c204e/cryptography-46.0.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:3b4995dc971c9fb83c25aa44cf45f02ba86f71ee600d81091c2f0cbae116b06c", size = 3476964, upload-time = "2026-02-10T19:18:20.687Z" }, - { url = "https://files.pythonhosted.org/packages/e9/6f/6cc6cc9955caa6eaf83660b0da2b077c7fe8ff9950a3c5e45d605038d439/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:bc84e875994c3b445871ea7181d424588171efec3e185dced958dad9e001950a", size = 4218321, upload-time = "2026-02-10T19:18:22.349Z" }, - { url = "https://files.pythonhosted.org/packages/3e/5d/c4da701939eeee699566a6c1367427ab91a8b7088cc2328c09dbee940415/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:2ae6971afd6246710480e3f15824ed3029a60fc16991db250034efd0b9fb4356", size = 4381786, upload-time = "2026-02-10T19:18:24.529Z" }, - { url = "https://files.pythonhosted.org/packages/ac/97/a538654732974a94ff96c1db621fa464f455c02d4bb7d2652f4edc21d600/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d861ee9e76ace6cf36a6a89b959ec08e7bc2493ee39d07ffe5acb23ef46d27da", size = 4217990, upload-time = "2026-02-10T19:18:25.957Z" }, - { url = "https://files.pythonhosted.org/packages/ae/11/7e500d2dd3ba891197b9efd2da5454b74336d64a7cc419aa7327ab74e5f6/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:2b7a67c9cd56372f3249b39699f2ad479f6991e62ea15800973b956f4b73e257", size = 4381252, upload-time = "2026-02-10T19:18:27.496Z" }, - { url = "https://files.pythonhosted.org/packages/bc/58/6b3d24e6b9bc474a2dcdee65dfd1f008867015408a271562e4b690561a4d/cryptography-46.0.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8456928655f856c6e1533ff59d5be76578a7157224dbd9ce6872f25055ab9ab7", size = 3407605, upload-time = "2026-02-10T19:18:29.233Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/47/93/ac8f3d5ff04d54bc814e961a43ae5b0b146154c89c61b47bb07557679b18/cryptography-46.0.7.tar.gz", hash = "sha256:e4cfd68c5f3e0bfdad0d38e023239b96a2fe84146481852dffbcca442c245aa5", size = 750652, upload-time = "2026-04-08T01:57:54.692Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/5d/4a8f770695d73be252331e60e526291e3df0c9b27556a90a6b47bccca4c2/cryptography-46.0.7-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:ea42cbe97209df307fdc3b155f1b6fa2577c0defa8f1f7d3be7d31d189108ad4", size = 7179869, upload-time = "2026-04-08T01:56:17.157Z" }, + { url = "https://files.pythonhosted.org/packages/5f/45/6d80dc379b0bbc1f9d1e429f42e4cb9e1d319c7a8201beffd967c516ea01/cryptography-46.0.7-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b36a4695e29fe69215d75960b22577197aca3f7a25b9cf9d165dcfe9d80bc325", size = 4275492, upload-time = "2026-04-08T01:56:19.36Z" }, + { url = "https://files.pythonhosted.org/packages/4a/9a/1765afe9f572e239c3469f2cb429f3ba7b31878c893b246b4b2994ffe2fe/cryptography-46.0.7-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5ad9ef796328c5e3c4ceed237a183f5d41d21150f972455a9d926593a1dcb308", size = 4426670, upload-time = "2026-04-08T01:56:21.415Z" }, + { url = "https://files.pythonhosted.org/packages/8f/3e/af9246aaf23cd4ee060699adab1e47ced3f5f7e7a8ffdd339f817b446462/cryptography-46.0.7-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:73510b83623e080a2c35c62c15298096e2a5dc8d51c3b4e1740211839d0dea77", size = 4280275, upload-time = "2026-04-08T01:56:23.539Z" }, + { url = "https://files.pythonhosted.org/packages/0f/54/6bbbfc5efe86f9d71041827b793c24811a017c6ac0fd12883e4caa86b8ed/cryptography-46.0.7-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cbd5fb06b62bd0721e1170273d3f4d5a277044c47ca27ee257025146c34cbdd1", size = 4928402, upload-time = "2026-04-08T01:56:25.624Z" }, + { url = "https://files.pythonhosted.org/packages/2d/cf/054b9d8220f81509939599c8bdbc0c408dbd2bdd41688616a20731371fe0/cryptography-46.0.7-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:420b1e4109cc95f0e5700eed79908cef9268265c773d3a66f7af1eef53d409ef", size = 4459985, upload-time = "2026-04-08T01:56:27.309Z" }, + { url = "https://files.pythonhosted.org/packages/f9/46/4e4e9c6040fb01c7467d47217d2f882daddeb8828f7df800cb806d8a2288/cryptography-46.0.7-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:24402210aa54baae71d99441d15bb5a1919c195398a87b563df84468160a65de", size = 3990652, upload-time = "2026-04-08T01:56:29.095Z" }, + { url = "https://files.pythonhosted.org/packages/36/5f/313586c3be5a2fbe87e4c9a254207b860155a8e1f3cca99f9910008e7d08/cryptography-46.0.7-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:8a469028a86f12eb7d2fe97162d0634026d92a21f3ae0ac87ed1c4a447886c83", size = 4279805, upload-time = "2026-04-08T01:56:30.928Z" }, + { url = "https://files.pythonhosted.org/packages/69/33/60dfc4595f334a2082749673386a4d05e4f0cf4df8248e63b2c3437585f2/cryptography-46.0.7-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9694078c5d44c157ef3162e3bf3946510b857df5a3955458381d1c7cfc143ddb", size = 4892883, upload-time = "2026-04-08T01:56:32.614Z" }, + { url = "https://files.pythonhosted.org/packages/c7/0b/333ddab4270c4f5b972f980adef4faa66951a4aaf646ca067af597f15563/cryptography-46.0.7-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:42a1e5f98abb6391717978baf9f90dc28a743b7d9be7f0751a6f56a75d14065b", size = 4459756, upload-time = "2026-04-08T01:56:34.306Z" }, + { url = "https://files.pythonhosted.org/packages/d2/14/633913398b43b75f1234834170947957c6b623d1701ffc7a9600da907e89/cryptography-46.0.7-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91bbcb08347344f810cbe49065914fe048949648f6bd5c2519f34619142bbe85", size = 4410244, upload-time = "2026-04-08T01:56:35.977Z" }, + { url = "https://files.pythonhosted.org/packages/10/f2/19ceb3b3dc14009373432af0c13f46aa08e3ce334ec6eff13492e1812ccd/cryptography-46.0.7-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5d1c02a14ceb9148cc7816249f64f623fbfee39e8c03b3650d842ad3f34d637e", size = 4674868, upload-time = "2026-04-08T01:56:38.034Z" }, + { url = "https://files.pythonhosted.org/packages/1a/bb/a5c213c19ee94b15dfccc48f363738633a493812687f5567addbcbba9f6f/cryptography-46.0.7-cp311-abi3-win32.whl", hash = "sha256:d23c8ca48e44ee015cd0a54aeccdf9f09004eba9fc96f38c911011d9ff1bd457", size = 3026504, upload-time = "2026-04-08T01:56:39.666Z" }, + { url = "https://files.pythonhosted.org/packages/2b/02/7788f9fefa1d060ca68717c3901ae7fffa21ee087a90b7f23c7a603c32ae/cryptography-46.0.7-cp311-abi3-win_amd64.whl", hash = "sha256:397655da831414d165029da9bc483bed2fe0e75dde6a1523ec2fe63f3c46046b", size = 3488363, upload-time = "2026-04-08T01:56:41.893Z" }, + { url = "https://files.pythonhosted.org/packages/7b/56/15619b210e689c5403bb0540e4cb7dbf11a6bf42e483b7644e471a2812b3/cryptography-46.0.7-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:d151173275e1728cf7839aaa80c34fe550c04ddb27b34f48c232193df8db5842", size = 7119671, upload-time = "2026-04-08T01:56:44Z" }, + { url = "https://files.pythonhosted.org/packages/74/66/e3ce040721b0b5599e175ba91ab08884c75928fbeb74597dd10ef13505d2/cryptography-46.0.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:db0f493b9181c7820c8134437eb8b0b4792085d37dbb24da050476ccb664e59c", size = 4268551, upload-time = "2026-04-08T01:56:46.071Z" }, + { url = "https://files.pythonhosted.org/packages/03/11/5e395f961d6868269835dee1bafec6a1ac176505a167f68b7d8818431068/cryptography-46.0.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ebd6daf519b9f189f85c479427bbd6e9c9037862cf8fe89ee35503bd209ed902", size = 4408887, upload-time = "2026-04-08T01:56:47.718Z" }, + { url = "https://files.pythonhosted.org/packages/40/53/8ed1cf4c3b9c8e611e7122fb56f1c32d09e1fff0f1d77e78d9ff7c82653e/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:b7b412817be92117ec5ed95f880defe9cf18a832e8cafacf0a22337dc1981b4d", size = 4271354, upload-time = "2026-04-08T01:56:49.312Z" }, + { url = "https://files.pythonhosted.org/packages/50/46/cf71e26025c2e767c5609162c866a78e8a2915bbcfa408b7ca495c6140c4/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:fbfd0e5f273877695cb93baf14b185f4878128b250cc9f8e617ea0c025dfb022", size = 4905845, upload-time = "2026-04-08T01:56:50.916Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ea/01276740375bac6249d0a971ebdf6b4dc9ead0ee0a34ef3b5a88c1a9b0d4/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:ffca7aa1d00cf7d6469b988c581598f2259e46215e0140af408966a24cf086ce", size = 4444641, upload-time = "2026-04-08T01:56:52.882Z" }, + { url = "https://files.pythonhosted.org/packages/3d/4c/7d258f169ae71230f25d9f3d06caabcff8c3baf0978e2b7d65e0acac3827/cryptography-46.0.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:60627cf07e0d9274338521205899337c5d18249db56865f943cbe753aa96f40f", size = 3967749, upload-time = "2026-04-08T01:56:54.597Z" }, + { url = "https://files.pythonhosted.org/packages/b5/2a/2ea0767cad19e71b3530e4cad9605d0b5e338b6a1e72c37c9c1ceb86c333/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:80406c3065e2c55d7f49a9550fe0c49b3f12e5bfff5dedb727e319e1afb9bf99", size = 4270942, upload-time = "2026-04-08T01:56:56.416Z" }, + { url = "https://files.pythonhosted.org/packages/41/3d/fe14df95a83319af25717677e956567a105bb6ab25641acaa093db79975d/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:c5b1ccd1239f48b7151a65bc6dd54bcfcc15e028c8ac126d3fada09db0e07ef1", size = 4871079, upload-time = "2026-04-08T01:56:58.31Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/4a479e0f36f8f378d397f4eab4c850b4ffb79a2f0d58704b8fa0703ddc11/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d5f7520159cd9c2154eb61eb67548ca05c5774d39e9c2c4339fd793fe7d097b2", size = 4443999, upload-time = "2026-04-08T01:57:00.508Z" }, + { url = "https://files.pythonhosted.org/packages/28/17/b59a741645822ec6d04732b43c5d35e4ef58be7bfa84a81e5ae6f05a1d33/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fcd8eac50d9138c1d7fc53a653ba60a2bee81a505f9f8850b6b2888555a45d0e", size = 4399191, upload-time = "2026-04-08T01:57:02.654Z" }, + { url = "https://files.pythonhosted.org/packages/59/6a/bb2e166d6d0e0955f1e9ff70f10ec4b2824c9cfcdb4da772c7dd69cc7d80/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:65814c60f8cc400c63131584e3e1fad01235edba2614b61fbfbfa954082db0ee", size = 4655782, upload-time = "2026-04-08T01:57:04.592Z" }, + { url = "https://files.pythonhosted.org/packages/95/b6/3da51d48415bcb63b00dc17c2eff3a651b7c4fed484308d0f19b30e8cb2c/cryptography-46.0.7-cp314-cp314t-win32.whl", hash = "sha256:fdd1736fed309b4300346f88f74cd120c27c56852c3838cab416e7a166f67298", size = 3002227, upload-time = "2026-04-08T01:57:06.91Z" }, + { url = "https://files.pythonhosted.org/packages/32/a8/9f0e4ed57ec9cebe506e58db11ae472972ecb0c659e4d52bbaee80ca340a/cryptography-46.0.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e06acf3c99be55aa3b516397fe42f5855597f430add9c17fa46bf2e0fb34c9bb", size = 3475332, upload-time = "2026-04-08T01:57:08.807Z" }, + { url = "https://files.pythonhosted.org/packages/a7/7f/cd42fc3614386bc0c12f0cb3c4ae1fc2bbca5c9662dfed031514911d513d/cryptography-46.0.7-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:462ad5cb1c148a22b2e3bcc5ad52504dff325d17daf5df8d88c17dda1f75f2a4", size = 7165618, upload-time = "2026-04-08T01:57:10.645Z" }, + { url = "https://files.pythonhosted.org/packages/a5/d0/36a49f0262d2319139d2829f773f1b97ef8aef7f97e6e5bd21455e5a8fb5/cryptography-46.0.7-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:84d4cced91f0f159a7ddacad249cc077e63195c36aac40b4150e7a57e84fffe7", size = 4270628, upload-time = "2026-04-08T01:57:12.885Z" }, + { url = "https://files.pythonhosted.org/packages/8a/6c/1a42450f464dda6ffbe578a911f773e54dd48c10f9895a23a7e88b3e7db5/cryptography-46.0.7-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:128c5edfe5e5938b86b03941e94fac9ee793a94452ad1365c9fc3f4f62216832", size = 4415405, upload-time = "2026-04-08T01:57:14.923Z" }, + { url = "https://files.pythonhosted.org/packages/9a/92/4ed714dbe93a066dc1f4b4581a464d2d7dbec9046f7c8b7016f5286329e2/cryptography-46.0.7-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5e51be372b26ef4ba3de3c167cd3d1022934bc838ae9eaad7e644986d2a3d163", size = 4272715, upload-time = "2026-04-08T01:57:16.638Z" }, + { url = "https://files.pythonhosted.org/packages/b7/e6/a26b84096eddd51494bba19111f8fffe976f6a09f132706f8f1bf03f51f7/cryptography-46.0.7-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cdf1a610ef82abb396451862739e3fc93b071c844399e15b90726ef7470eeaf2", size = 4918400, upload-time = "2026-04-08T01:57:19.021Z" }, + { url = "https://files.pythonhosted.org/packages/c7/08/ffd537b605568a148543ac3c2b239708ae0bd635064bab41359252ef88ed/cryptography-46.0.7-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1d25aee46d0c6f1a501adcddb2d2fee4b979381346a78558ed13e50aa8a59067", size = 4450634, upload-time = "2026-04-08T01:57:21.185Z" }, + { url = "https://files.pythonhosted.org/packages/16/01/0cd51dd86ab5b9befe0d031e276510491976c3a80e9f6e31810cce46c4ad/cryptography-46.0.7-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:cdfbe22376065ffcf8be74dc9a909f032df19bc58a699456a21712d6e5eabfd0", size = 3985233, upload-time = "2026-04-08T01:57:22.862Z" }, + { url = "https://files.pythonhosted.org/packages/92/49/819d6ed3a7d9349c2939f81b500a738cb733ab62fbecdbc1e38e83d45e12/cryptography-46.0.7-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:abad9dac36cbf55de6eb49badd4016806b3165d396f64925bf2999bcb67837ba", size = 4271955, upload-time = "2026-04-08T01:57:24.814Z" }, + { url = "https://files.pythonhosted.org/packages/80/07/ad9b3c56ebb95ed2473d46df0847357e01583f4c52a85754d1a55e29e4d0/cryptography-46.0.7-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:935ce7e3cfdb53e3536119a542b839bb94ec1ad081013e9ab9b7cfd478b05006", size = 4879888, upload-time = "2026-04-08T01:57:26.88Z" }, + { url = "https://files.pythonhosted.org/packages/b8/c7/201d3d58f30c4c2bdbe9b03844c291feb77c20511cc3586daf7edc12a47b/cryptography-46.0.7-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:35719dc79d4730d30f1c2b6474bd6acda36ae2dfae1e3c16f2051f215df33ce0", size = 4449961, upload-time = "2026-04-08T01:57:29.068Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ef/649750cbf96f3033c3c976e112265c33906f8e462291a33d77f90356548c/cryptography-46.0.7-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7bbc6ccf49d05ac8f7d7b5e2e2c33830d4fe2061def88210a126d130d7f71a85", size = 4401696, upload-time = "2026-04-08T01:57:31.029Z" }, + { url = "https://files.pythonhosted.org/packages/41/52/a8908dcb1a389a459a29008c29966c1d552588d4ae6d43f3a1a4512e0ebe/cryptography-46.0.7-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a1529d614f44b863a7b480c6d000fe93b59acee9c82ffa027cfadc77521a9f5e", size = 4664256, upload-time = "2026-04-08T01:57:33.144Z" }, + { url = "https://files.pythonhosted.org/packages/4b/fa/f0ab06238e899cc3fb332623f337a7364f36f4bb3f2534c2bb95a35b132c/cryptography-46.0.7-cp38-abi3-win32.whl", hash = "sha256:f247c8c1a1fb45e12586afbb436ef21ff1e80670b2861a90353d9b025583d246", size = 3013001, upload-time = "2026-04-08T01:57:34.933Z" }, + { url = "https://files.pythonhosted.org/packages/d2/f1/00ce3bde3ca542d1acd8f8cfa38e446840945aa6363f9b74746394b14127/cryptography-46.0.7-cp38-abi3-win_amd64.whl", hash = "sha256:506c4ff91eff4f82bdac7633318a526b1d1309fc07ca76a3ad182cb5b686d6d3", size = 3472985, upload-time = "2026-04-08T01:57:36.714Z" }, + { url = "https://files.pythonhosted.org/packages/63/0c/dca8abb64e7ca4f6b2978769f6fea5ad06686a190cec381f0a796fdcaaba/cryptography-46.0.7-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:fc9ab8856ae6cf7c9358430e49b368f3108f050031442eaeb6b9d87e4dcf4e4f", size = 3476879, upload-time = "2026-04-08T01:57:38.664Z" }, + { url = "https://files.pythonhosted.org/packages/3a/ea/075aac6a84b7c271578d81a2f9968acb6e273002408729f2ddff517fed4a/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d3b99c535a9de0adced13d159c5a9cf65c325601aa30f4be08afd680643e9c15", size = 4219700, upload-time = "2026-04-08T01:57:40.625Z" }, + { url = "https://files.pythonhosted.org/packages/6c/7b/1c55db7242b5e5612b29fc7a630e91ee7a6e3c8e7bf5406d22e206875fbd/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d02c738dacda7dc2a74d1b2b3177042009d5cab7c7079db74afc19e56ca1b455", size = 4385982, upload-time = "2026-04-08T01:57:42.725Z" }, + { url = "https://files.pythonhosted.org/packages/cb/da/9870eec4b69c63ef5925bf7d8342b7e13bc2ee3d47791461c4e49ca212f4/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:04959522f938493042d595a736e7dbdff6eb6cc2339c11465b3ff89343b65f65", size = 4219115, upload-time = "2026-04-08T01:57:44.939Z" }, + { url = "https://files.pythonhosted.org/packages/f4/72/05aa5832b82dd341969e9a734d1812a6aadb088d9eb6f0430fc337cc5a8f/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:3986ac1dee6def53797289999eabe84798ad7817f3e97779b5061a95b0ee4968", size = 4385479, upload-time = "2026-04-08T01:57:46.86Z" }, + { url = "https://files.pythonhosted.org/packages/20/2a/1b016902351a523aa2bd446b50a5bc1175d7a7d1cf90fe2ef904f9b84ebc/cryptography-46.0.7-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:258514877e15963bd43b558917bc9f54cf7cf866c38aa576ebf47a77ddbc43a4", size = 3412829, upload-time = "2026-04-08T01:57:48.874Z" }, ] [[package]] @@ -601,7 +622,7 @@ wheels = [ [[package]] name = "gevent" -version = "25.9.1" +version = "26.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation == 'CPython' and sys_platform == 'win32'" }, @@ -609,91 +630,96 @@ dependencies = [ { name = "zope-event" }, { name = "zope-interface" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9e/48/b3ef2673ffb940f980966694e40d6d32560f3ffa284ecaeb5ea3a90a6d3f/gevent-25.9.1.tar.gz", hash = "sha256:adf9cd552de44a4e6754c51ff2e78d9193b7fa6eab123db9578a210e657235dd", size = 5059025, upload-time = "2025-09-17T16:15:34.528Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/81/86/03f8db0704fed41b0fa830425845f1eb4e20c92efa3f18751ee17809e9c6/gevent-25.9.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:18e5aff9e8342dc954adb9c9c524db56c2f3557999463445ba3d9cbe3dada7b7", size = 1792418, upload-time = "2025-09-17T15:41:24.384Z" }, - { url = "https://files.pythonhosted.org/packages/5f/35/f6b3a31f0849a62cfa2c64574bcc68a781d5499c3195e296e892a121a3cf/gevent-25.9.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1cdf6db28f050ee103441caa8b0448ace545364f775059d5e2de089da975c457", size = 1875700, upload-time = "2025-09-17T15:48:59.652Z" }, - { url = "https://files.pythonhosted.org/packages/66/1e/75055950aa9b48f553e061afa9e3728061b5ccecca358cef19166e4ab74a/gevent-25.9.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:812debe235a8295be3b2a63b136c2474241fa5c58af55e6a0f8cfc29d4936235", size = 1831365, upload-time = "2025-09-17T15:49:19.426Z" }, - { url = "https://files.pythonhosted.org/packages/31/e8/5c1f6968e5547e501cfa03dcb0239dff55e44c3660a37ec534e32a0c008f/gevent-25.9.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b28b61ff9216a3d73fe8f35669eefcafa957f143ac534faf77e8a19eb9e6883a", size = 2122087, upload-time = "2025-09-17T15:15:12.329Z" }, - { url = "https://files.pythonhosted.org/packages/c0/2c/ebc5d38a7542af9fb7657bfe10932a558bb98c8a94e4748e827d3823fced/gevent-25.9.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5e4b6278b37373306fc6b1e5f0f1cf56339a1377f67c35972775143d8d7776ff", size = 1808776, upload-time = "2025-09-17T15:52:40.16Z" }, - { url = "https://files.pythonhosted.org/packages/e6/26/e1d7d6c8ffbf76fe1fbb4e77bdb7f47d419206adc391ec40a8ace6ebbbf0/gevent-25.9.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d99f0cb2ce43c2e8305bf75bee61a8bde06619d21b9d0316ea190fc7a0620a56", size = 2179141, upload-time = "2025-09-17T15:24:09.895Z" }, - { url = "https://files.pythonhosted.org/packages/1d/6c/bb21fd9c095506aeeaa616579a356aa50935165cc0f1e250e1e0575620a7/gevent-25.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:72152517ecf548e2f838c61b4be76637d99279dbaa7e01b3924df040aa996586", size = 1677941, upload-time = "2025-09-17T19:59:50.185Z" }, - { url = "https://files.pythonhosted.org/packages/f7/49/e55930ba5259629eb28ac7ee1abbca971996a9165f902f0249b561602f24/gevent-25.9.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:46b188248c84ffdec18a686fcac5dbb32365d76912e14fda350db5dc0bfd4f86", size = 2955991, upload-time = "2025-09-17T14:52:30.568Z" }, - { url = "https://files.pythonhosted.org/packages/aa/88/63dc9e903980e1da1e16541ec5c70f2b224ec0a8e34088cb42794f1c7f52/gevent-25.9.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f2b54ea3ca6f0c763281cd3f96010ac7e98c2e267feb1221b5a26e2ca0b9a692", size = 1808503, upload-time = "2025-09-17T15:41:25.59Z" }, - { url = "https://files.pythonhosted.org/packages/7a/8d/7236c3a8f6ef7e94c22e658397009596fa90f24c7d19da11ad7ab3a9248e/gevent-25.9.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7a834804ac00ed8a92a69d3826342c677be651b1c3cd66cc35df8bc711057aa2", size = 1890001, upload-time = "2025-09-17T15:49:01.227Z" }, - { url = "https://files.pythonhosted.org/packages/4f/63/0d7f38c4a2085ecce26b50492fc6161aa67250d381e26d6a7322c309b00f/gevent-25.9.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:323a27192ec4da6b22a9e51c3d9d896ff20bc53fdc9e45e56eaab76d1c39dd74", size = 1855335, upload-time = "2025-09-17T15:49:20.582Z" }, - { url = "https://files.pythonhosted.org/packages/95/18/da5211dfc54c7a57e7432fd9a6ffeae1ce36fe5a313fa782b1c96529ea3d/gevent-25.9.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6ea78b39a2c51d47ff0f130f4c755a9a4bbb2dd9721149420ad4712743911a51", size = 2109046, upload-time = "2025-09-17T15:15:13.817Z" }, - { url = "https://files.pythonhosted.org/packages/a6/5a/7bb5ec8e43a2c6444853c4a9f955f3e72f479d7c24ea86c95fb264a2de65/gevent-25.9.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:dc45cd3e1cc07514a419960af932a62eb8515552ed004e56755e4bf20bad30c5", size = 1827099, upload-time = "2025-09-17T15:52:41.384Z" }, - { url = "https://files.pythonhosted.org/packages/ca/d4/b63a0a60635470d7d986ef19897e893c15326dd69e8fb342c76a4f07fe9e/gevent-25.9.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34e01e50c71eaf67e92c186ee0196a039d6e4f4b35670396baed4a2d8f1b347f", size = 2172623, upload-time = "2025-09-17T15:24:12.03Z" }, - { url = "https://files.pythonhosted.org/packages/d5/98/caf06d5d22a7c129c1fb2fc1477306902a2c8ddfd399cd26bbbd4caf2141/gevent-25.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:4acd6bcd5feabf22c7c5174bd3b9535ee9f088d2bbce789f740ad8d6554b18f3", size = 1682837, upload-time = "2025-09-17T19:48:47.318Z" }, - { url = "https://files.pythonhosted.org/packages/5a/77/b97f086388f87f8ad3e01364f845004aef0123d4430241c7c9b1f9bde742/gevent-25.9.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:4f84591d13845ee31c13f44bdf6bd6c3dbf385b5af98b2f25ec328213775f2ed", size = 2973739, upload-time = "2025-09-17T14:53:30.279Z" }, - { url = "https://files.pythonhosted.org/packages/3c/2e/9d5f204ead343e5b27bbb2fedaec7cd0009d50696b2266f590ae845d0331/gevent-25.9.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9cdbb24c276a2d0110ad5c978e49daf620b153719ac8a548ce1250a7eb1b9245", size = 1809165, upload-time = "2025-09-17T15:41:27.193Z" }, - { url = "https://files.pythonhosted.org/packages/10/3e/791d1bf1eb47748606d5f2c2aa66571f474d63e0176228b1f1fd7b77ab37/gevent-25.9.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:88b6c07169468af631dcf0fdd3658f9246d6822cc51461d43f7c44f28b0abb82", size = 1890638, upload-time = "2025-09-17T15:49:02.45Z" }, - { url = "https://files.pythonhosted.org/packages/f2/5c/9ad0229b2b4d81249ca41e4f91dd8057deaa0da6d4fbe40bf13cdc5f7a47/gevent-25.9.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b7bb0e29a7b3e6ca9bed2394aa820244069982c36dc30b70eb1004dd67851a48", size = 1857118, upload-time = "2025-09-17T15:49:22.125Z" }, - { url = "https://files.pythonhosted.org/packages/49/2a/3010ed6c44179a3a5c5c152e6de43a30ff8bc2c8de3115ad8733533a018f/gevent-25.9.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2951bb070c0ee37b632ac9134e4fdaad70d2e660c931bb792983a0837fe5b7d7", size = 2111598, upload-time = "2025-09-17T15:15:15.226Z" }, - { url = "https://files.pythonhosted.org/packages/08/75/6bbe57c19a7aa4527cc0f9afcdf5a5f2aed2603b08aadbccb5bf7f607ff4/gevent-25.9.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e4e17c2d57e9a42e25f2a73d297b22b60b2470a74be5a515b36c984e1a246d47", size = 1829059, upload-time = "2025-09-17T15:52:42.596Z" }, - { url = "https://files.pythonhosted.org/packages/06/6e/19a9bee9092be45679cb69e4dd2e0bf5f897b7140b4b39c57cc123d24829/gevent-25.9.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8d94936f8f8b23d9de2251798fcb603b84f083fdf0d7f427183c1828fb64f117", size = 2173529, upload-time = "2025-09-17T15:24:13.897Z" }, - { url = "https://files.pythonhosted.org/packages/ca/4f/50de9afd879440e25737e63f5ba6ee764b75a3abe17376496ab57f432546/gevent-25.9.1-cp313-cp313-win_amd64.whl", hash = "sha256:eb51c5f9537b07da673258b4832f6635014fee31690c3f0944d34741b69f92fa", size = 1681518, upload-time = "2025-09-17T19:39:47.488Z" }, - { url = "https://files.pythonhosted.org/packages/15/1a/948f8167b2cdce573cf01cec07afc64d0456dc134b07900b26ac7018b37e/gevent-25.9.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:1a3fe4ea1c312dbf6b375b416925036fe79a40054e6bf6248ee46526ea628be1", size = 2982934, upload-time = "2025-09-17T14:54:11.302Z" }, - { url = "https://files.pythonhosted.org/packages/9b/ec/726b146d1d3aad82e03d2e1e1507048ab6072f906e83f97f40667866e582/gevent-25.9.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0adb937f13e5fb90cca2edf66d8d7e99d62a299687400ce2edee3f3504009356", size = 1813982, upload-time = "2025-09-17T15:41:28.506Z" }, - { url = "https://files.pythonhosted.org/packages/35/5d/5f83f17162301662bd1ce702f8a736a8a8cac7b7a35e1d8b9866938d1f9d/gevent-25.9.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:427f869a2050a4202d93cf7fd6ab5cffb06d3e9113c10c967b6e2a0d45237cb8", size = 1894902, upload-time = "2025-09-17T15:49:03.702Z" }, - { url = "https://files.pythonhosted.org/packages/83/cd/cf5e74e353f60dab357829069ffc300a7bb414c761f52cf8c0c6e9728b8d/gevent-25.9.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c049880175e8c93124188f9d926af0a62826a3b81aa6d3074928345f8238279e", size = 1861792, upload-time = "2025-09-17T15:49:23.279Z" }, - { url = "https://files.pythonhosted.org/packages/dd/65/b9a4526d4a4edce26fe4b3b993914ec9dc64baabad625a3101e51adb17f3/gevent-25.9.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b5a67a0974ad9f24721034d1e008856111e0535f1541499f72a733a73d658d1c", size = 2113215, upload-time = "2025-09-17T15:15:16.34Z" }, - { url = "https://files.pythonhosted.org/packages/e5/be/7d35731dfaf8370795b606e515d964a0967e129db76ea7873f552045dd39/gevent-25.9.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1d0f5d8d73f97e24ea8d24d8be0f51e0cf7c54b8021c1fddb580bf239474690f", size = 1833449, upload-time = "2025-09-17T15:52:43.75Z" }, - { url = "https://files.pythonhosted.org/packages/65/58/7bc52544ea5e63af88c4a26c90776feb42551b7555a1c89c20069c168a3f/gevent-25.9.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ddd3ff26e5c4240d3fbf5516c2d9d5f2a998ef87cfb73e1429cfaeaaec860fa6", size = 2176034, upload-time = "2025-09-17T15:24:15.676Z" }, - { url = "https://files.pythonhosted.org/packages/c2/69/a7c4ba2ffbc7c7dbf6d8b4f5d0f0a421f7815d229f4909854266c445a3d4/gevent-25.9.1-cp314-cp314-win_amd64.whl", hash = "sha256:bb63c0d6cb9950cc94036a4995b9cc4667b8915366613449236970f4394f94d7", size = 1703019, upload-time = "2025-09-17T19:30:55.272Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/20/27/1062fa31333dc3428a1f5f33cd6598b0552165ba679ca3ba116de42c9e8e/gevent-26.4.0.tar.gz", hash = "sha256:288d03addfccf0d1c67268358b6759b04392bf3bc35d26f3d9a45c82899c292d", size = 6242440, upload-time = "2026-04-09T12:08:19.482Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/0e/330c4788860520850b7f4c6f84dd8591df5172cfd3f2796c046704ee879e/gevent-26.4.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:201323a5fb9a0646a0c7b384395ca55d60ee83200677919229df0648c4b78e6c", size = 1767278, upload-time = "2026-04-08T22:23:16.491Z" }, + { url = "https://files.pythonhosted.org/packages/cf/27/717593d7cce74a2fd6bee0713793518e0398132303d5267f02dd587c5945/gevent-26.4.0-cp311-cp311-manylinux_2_28_ppc64le.whl", hash = "sha256:82d68a60a4207826db295b4e80a204c9d392ce78ccc15679195faeb9e29d8388", size = 1861609, upload-time = "2026-04-08T22:27:09.302Z" }, + { url = "https://files.pythonhosted.org/packages/fb/af/ce6d4d554d9afc354b46b78eccb032f6add4d27c3eadaa0201ee103fa831/gevent-26.4.0-cp311-cp311-manylinux_2_28_s390x.whl", hash = "sha256:35b037b415ed38369717800250fe5974249525953b46026bef9def20f946dfb0", size = 1803675, upload-time = "2026-04-08T22:34:35.289Z" }, + { url = "https://files.pythonhosted.org/packages/57/09/909166fff7d2ab9523e93bbd56e863df79a856b2857350218be83aef119d/gevent-26.4.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:6d973735d2067607a32cd182893978755eee829a0dc268087592d3b715e63fad", size = 2118034, upload-time = "2026-04-08T21:54:13.293Z" }, + { url = "https://files.pythonhosted.org/packages/83/6b/ab68e1cc09fd6dd7adb9e1c54a47c6328df20aa012ed75526a3244f2ad05/gevent-26.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4fbd3ff28a7babbfee750684c4f46ba6eedb3bce69365dd146726986b79fa6c1", size = 1777768, upload-time = "2026-04-08T22:26:51.87Z" }, + { url = "https://files.pythonhosted.org/packages/ff/d3/b75568e7206ea4b89a7e21750381aa4a6f9afcced41d5a80a72b5fcc6b87/gevent-26.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f0a03650ca60c4c5774cbe21333905b95f2f5abd98ea5a3dbf28d93f2a7a5a84", size = 2144355, upload-time = "2026-04-08T22:00:00.684Z" }, + { url = "https://files.pythonhosted.org/packages/f7/32/5f795143351bfbecd05467deec48ea75416bb90eb7a6dd042c7d7e5ec594/gevent-26.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d00d8c4ca1afab90e478b79679dd53787082c6da8d2f4fdc7667a4440d1e1a7a", size = 1676684, upload-time = "2026-04-08T23:28:04.124Z" }, + { url = "https://files.pythonhosted.org/packages/3d/16/131d3874f50974b355c90a061a12d3fe2292cde0f875a1fa3d8b224f1251/gevent-26.4.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:318a0a73f664113e8d86d0cb0e328e7650e2d7d9c2e045418ab6fb1285831ad3", size = 2928699, upload-time = "2026-04-08T21:25:36.215Z" }, + { url = "https://files.pythonhosted.org/packages/ea/8b/199e59b303adaff7f7365def9ab569c7ecd863363c974548bce3ddc2c89d/gevent-26.4.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:ce7aa033a3f68beb6732d1450a80c1af29e63e0c2d01abad7918cf2507f72fa6", size = 1783821, upload-time = "2026-04-08T22:23:18.73Z" }, + { url = "https://files.pythonhosted.org/packages/e2/2d/b8249c9bd3f386191311c3a9bec4068e192a3f9df2fad92a71a15265ba15/gevent-26.4.0-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:a1b897c952baefd72232efaeb3bdb1ca2fa7ae94cbfe68ac21201b03e843190a", size = 1879424, upload-time = "2026-04-08T22:27:10.561Z" }, + { url = "https://files.pythonhosted.org/packages/ef/89/59216985c1f2c11f2f28bbc88e583588ad44cdde823c530ad4e307be6612/gevent-26.4.0-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:7eef2ea508ce41795e20587a5fc868ae4919543097c81a40fbdfd65bc479f54f", size = 1830575, upload-time = "2026-04-08T22:34:37.093Z" }, + { url = "https://files.pythonhosted.org/packages/ee/a9/2d67d2b0aa0ca9d7bb7fe73c3bbb97b3695cb15c338a6ea7734f58da9add/gevent-26.4.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:f7e12fdd28cc9f39a463d8df5172d698c64a8ed385a21d98e7092fd8308a139a", size = 2113898, upload-time = "2026-04-08T21:54:14.9Z" }, + { url = "https://files.pythonhosted.org/packages/95/a3/457d58d9b3e7da17c8456d841c37a32af8d231a1d71237ad201b19129317/gevent-26.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d48e3ee13d7678c24c22f19d441ad6bc220a79f23662d03ff36fae0d62efdb59", size = 1795890, upload-time = "2026-04-08T22:26:53.252Z" }, + { url = "https://files.pythonhosted.org/packages/a7/cc/cbe78f2626643b20275aaa41cd2cc45ba75056e3665bde36bc190af3cae0/gevent-26.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c58c8e034f94329be4dc0979fba3301005a433dbab42cea0b2c33fd736946872", size = 2139791, upload-time = "2026-04-08T22:00:02.375Z" }, + { url = "https://files.pythonhosted.org/packages/f6/df/7875e08b06a95f4577b71708ec470d029fadf873a66eb813a2861d79dfb5/gevent-26.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:1c737e6ac6ce1398df0e3f41c58d982e397c993cbe73ac05b7edbe39e128c9cb", size = 1680530, upload-time = "2026-04-08T23:15:38.714Z" }, + { url = "https://files.pythonhosted.org/packages/3d/be/51809d98bb00846d7756a0b82625024f9302145f3d024846b43f05efeddb/gevent-26.4.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:1fe581d41c63cd1d8b12c69561ce53a48ad0d8763b254740d7bfea997335a38c", size = 2951507, upload-time = "2026-04-08T21:25:25.809Z" }, + { url = "https://files.pythonhosted.org/packages/d6/86/89325a62a4e8cc1934e155b383b66491ed21d1e774b13d5054d51fa0ac81/gevent-26.4.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:c503b0c0a681e795255a13e5bb4e41615c3b020c1db93b8dfa04cfeb8f19d5a9", size = 1786029, upload-time = "2026-04-08T22:23:20.395Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ea/04d112844aa992da583cbd280f17a4ba097da338dab347efd0aa5e235645/gevent-26.4.0-cp313-cp313-manylinux_2_28_ppc64le.whl", hash = "sha256:684256c29e3e5d4d0c4d06b772d00574d0dc859dfbb2fd13d318c512b16e1f89", size = 1881326, upload-time = "2026-04-08T22:27:11.822Z" }, + { url = "https://files.pythonhosted.org/packages/a1/33/71900c5ba442f5df89456b6d9fdaa43da2ae7cdd937d8c5667b49323ceb4/gevent-26.4.0-cp313-cp313-manylinux_2_28_s390x.whl", hash = "sha256:73eafd06b158d511f1ec6e5902a45e0ae3b48e745f35e9df97d25f809f537d88", size = 1833123, upload-time = "2026-04-08T22:34:39.001Z" }, + { url = "https://files.pythonhosted.org/packages/d0/af/7df19c92e56842921f34787e1168c7afc52a23b0d1253bba99344809a935/gevent-26.4.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:1a18e543c830a1c07a2efeb33786a57ccac360af70cb42bbaf5a6f5f7ca49300", size = 2114330, upload-time = "2026-04-08T21:54:16.547Z" }, + { url = "https://files.pythonhosted.org/packages/ca/0e/202694960f8d4dda68fd2a73bbcb8251e2d5308339924310ff1fff31bf7c/gevent-26.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:74f1e3a460c43aefcb4ff9ef91aac15abc0b42e5233771e1956574d14ba9cac6", size = 1798427, upload-time = "2026-04-08T22:26:54.462Z" }, + { url = "https://files.pythonhosted.org/packages/75/69/2d056b2a4e3ef1f65f94002725572d1e99163ff79231dbb68ad529e7cb9d/gevent-26.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:954258873ae0bcc97fb41e48db25284fb73454bfefe27db8ceb89225da5502fb", size = 2140100, upload-time = "2026-04-08T22:00:03.966Z" }, + { url = "https://files.pythonhosted.org/packages/fe/a0/1a7f64aa2476c2b44abaecca919a6561bda85234f99fc7ac3c66bcb93050/gevent-26.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:4a9a64c064457c1afaf93ee2815fe0f38be6ecbb92806a6a712f12afc3e26cf5", size = 1680206, upload-time = "2026-04-08T23:01:56.636Z" }, + { url = "https://files.pythonhosted.org/packages/7e/f3/64638a941988f09aa1816e2674eb1efb215b6fa64a97edef6e25177b0845/gevent-26.4.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:7ab0f183a6fd2369eef619832eef14f1f2f69c605163c3f2dc41deb799af4a71", size = 2967206, upload-time = "2026-04-08T21:25:44.73Z" }, + { url = "https://files.pythonhosted.org/packages/f6/70/a86be65a51d3ebb92c82a70adc9c5c32b1a9d9579120d0be1db7cf534ce0/gevent-26.4.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:7e5906860e632bf965e1966c57e6bfc19dcb79dc262f04fdb0a9d7c12147bf69", size = 1792916, upload-time = "2026-04-08T22:23:22.006Z" }, + { url = "https://files.pythonhosted.org/packages/40/92/18fdb4b28f20129395f1c041773adee99e7fc2bcfff216df93bfb80787d5/gevent-26.4.0-cp314-cp314-manylinux_2_28_ppc64le.whl", hash = "sha256:297a361071dc6708115d4544859321e93b02a6cd5823ba02c0a909530a519d45", size = 1886617, upload-time = "2026-04-08T22:27:13.716Z" }, + { url = "https://files.pythonhosted.org/packages/af/c9/d02222ecf79d10c8a0c2755661485395b58c4bfffaafd88bcc230ce392de/gevent-26.4.0-cp314-cp314-manylinux_2_28_s390x.whl", hash = "sha256:7e74f59e5c9011afa2a9cb7106bb9a59f2a1f74c3d7b272c1b852eb0bc0b8f90", size = 1837660, upload-time = "2026-04-08T22:34:40.823Z" }, + { url = "https://files.pythonhosted.org/packages/46/85/9376d125fa4f7b0f269925d0d622eda0ff8f8dfc8d0c097a096c511fc738/gevent-26.4.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:45d6010a6a981f5a2b3411c4e38fbe305a1b46e4b12db3b4914775927dea7ba4", size = 2119342, upload-time = "2026-04-08T21:54:17.747Z" }, + { url = "https://files.pythonhosted.org/packages/b1/c4/1fe2817daca8e97c365fd739dd4057f71cce26ef600fb8465deb8060c83c/gevent-26.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:dc38137ba2f43794c488615aafa2eefd0cc142f484a8274d4c827ed7a031a1e2", size = 1805672, upload-time = "2026-04-08T22:26:55.792Z" }, + { url = "https://files.pythonhosted.org/packages/e7/cc/ccbcbd56e7e85482291fbb90a317f5febf630ec4174a91506f4167ba0912/gevent-26.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:29a225d2d4da37e20c7a246754a64442d0e43e4534b8cc764f89530bb22a4237", size = 2145594, upload-time = "2026-04-08T22:00:05.275Z" }, + { url = "https://files.pythonhosted.org/packages/8e/b9/7dd37b6001d16f692b1bfb6e68cad642beb38b34a753c29bbff312f46e4b/gevent-26.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:d1c08bc9bb6bd79732a26710a99588b5e9b67b668e165dd609704b876f41baab", size = 1703189, upload-time = "2026-04-08T22:48:31.713Z" }, ] [[package]] name = "greenlet" -version = "3.3.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a3/51/1664f6b78fc6ebbd98019a1fd730e83fa78f2db7058f72b1463d3612b8db/greenlet-3.3.2.tar.gz", hash = "sha256:2eaf067fc6d886931c7962e8c6bede15d2f01965560f3359b27c80bde2d151f2", size = 188267, upload-time = "2026-02-20T20:54:15.531Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f3/47/16400cb42d18d7a6bb46f0626852c1718612e35dcb0dffa16bbaffdf5dd2/greenlet-3.3.2-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:c56692189a7d1c7606cb794be0a8381470d95c57ce5be03fb3d0ef57c7853b86", size = 278890, upload-time = "2026-02-20T20:19:39.263Z" }, - { url = "https://files.pythonhosted.org/packages/a3/90/42762b77a5b6aa96cd8c0e80612663d39211e8ae8a6cd47c7f1249a66262/greenlet-3.3.2-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ebd458fa8285960f382841da585e02201b53a5ec2bac6b156fc623b5ce4499f", size = 581120, upload-time = "2026-02-20T20:47:30.161Z" }, - { url = "https://files.pythonhosted.org/packages/bf/6f/f3d64f4fa0a9c7b5c5b3c810ff1df614540d5aa7d519261b53fba55d4df9/greenlet-3.3.2-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a443358b33c4ec7b05b79a7c8b466f5d275025e750298be7340f8fc63dff2a55", size = 594363, upload-time = "2026-02-20T20:55:56.965Z" }, - { url = "https://files.pythonhosted.org/packages/9c/8b/1430a04657735a3f23116c2e0d5eb10220928846e4537a938a41b350bed6/greenlet-3.3.2-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4375a58e49522698d3e70cc0b801c19433021b5c37686f7ce9c65b0d5c8677d2", size = 605046, upload-time = "2026-02-20T21:02:45.234Z" }, - { url = "https://files.pythonhosted.org/packages/72/83/3e06a52aca8128bdd4dcd67e932b809e76a96ab8c232a8b025b2850264c5/greenlet-3.3.2-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e2cd90d413acbf5e77ae41e5d3c9b3ac1d011a756d7284d7f3f2b806bbd6358", size = 594156, upload-time = "2026-02-20T20:20:59.955Z" }, - { url = "https://files.pythonhosted.org/packages/70/79/0de5e62b873e08fe3cef7dbe84e5c4bc0e8ed0c7ff131bccb8405cd107c8/greenlet-3.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:442b6057453c8cb29b4fb36a2ac689382fc71112273726e2423f7f17dc73bf99", size = 1554649, upload-time = "2026-02-20T20:49:32.293Z" }, - { url = "https://files.pythonhosted.org/packages/5a/00/32d30dee8389dc36d42170a9c66217757289e2afb0de59a3565260f38373/greenlet-3.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:45abe8eb6339518180d5a7fa47fa01945414d7cca5ecb745346fc6a87d2750be", size = 1619472, upload-time = "2026-02-20T20:21:07.966Z" }, - { url = "https://files.pythonhosted.org/packages/f1/3a/efb2cf697fbccdf75b24e2c18025e7dfa54c4f31fab75c51d0fe79942cef/greenlet-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e692b2dae4cc7077cbb11b47d258533b48c8fde69a33d0d8a82e2fe8d8531d5", size = 230389, upload-time = "2026-02-20T20:17:18.772Z" }, - { url = "https://files.pythonhosted.org/packages/e1/a1/65bbc059a43a7e2143ec4fc1f9e3f673e04f9c7b371a494a101422ac4fd5/greenlet-3.3.2-cp311-cp311-win_arm64.whl", hash = "sha256:02b0a8682aecd4d3c6c18edf52bc8e51eacdd75c8eac52a790a210b06aa295fd", size = 229645, upload-time = "2026-02-20T20:18:18.695Z" }, - { url = "https://files.pythonhosted.org/packages/ea/ab/1608e5a7578e62113506740b88066bf09888322a311cff602105e619bd87/greenlet-3.3.2-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:ac8d61d4343b799d1e526db579833d72f23759c71e07181c2d2944e429eb09cd", size = 280358, upload-time = "2026-02-20T20:17:43.971Z" }, - { url = "https://files.pythonhosted.org/packages/a5/23/0eae412a4ade4e6623ff7626e38998cb9b11e9ff1ebacaa021e4e108ec15/greenlet-3.3.2-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ceec72030dae6ac0c8ed7591b96b70410a8be370b6a477b1dbc072856ad02bd", size = 601217, upload-time = "2026-02-20T20:47:31.462Z" }, - { url = "https://files.pythonhosted.org/packages/f8/16/5b1678a9c07098ecb9ab2dd159fafaf12e963293e61ee8d10ecb55273e5e/greenlet-3.3.2-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a2a5be83a45ce6188c045bcc44b0ee037d6a518978de9a5d97438548b953a1ac", size = 611792, upload-time = "2026-02-20T20:55:58.423Z" }, - { url = "https://files.pythonhosted.org/packages/5c/c5/cc09412a29e43406eba18d61c70baa936e299bc27e074e2be3806ed29098/greenlet-3.3.2-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ae9e21c84035c490506c17002f5c8ab25f980205c3e61ddb3a2a2a2e6c411fcb", size = 626250, upload-time = "2026-02-20T21:02:46.596Z" }, - { url = "https://files.pythonhosted.org/packages/50/1f/5155f55bd71cabd03765a4aac9ac446be129895271f73872c36ebd4b04b6/greenlet-3.3.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43e99d1749147ac21dde49b99c9abffcbc1e2d55c67501465ef0930d6e78e070", size = 613875, upload-time = "2026-02-20T20:21:01.102Z" }, - { url = "https://files.pythonhosted.org/packages/fc/dd/845f249c3fcd69e32df80cdab059b4be8b766ef5830a3d0aa9d6cad55beb/greenlet-3.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4c956a19350e2c37f2c48b336a3afb4bff120b36076d9d7fb68cb44e05d95b79", size = 1571467, upload-time = "2026-02-20T20:49:33.495Z" }, - { url = "https://files.pythonhosted.org/packages/2a/50/2649fe21fcc2b56659a452868e695634722a6655ba245d9f77f5656010bf/greenlet-3.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6c6f8ba97d17a1e7d664151284cb3315fc5f8353e75221ed4324f84eb162b395", size = 1640001, upload-time = "2026-02-20T20:21:09.154Z" }, - { url = "https://files.pythonhosted.org/packages/9b/40/cc802e067d02af8b60b6771cea7d57e21ef5e6659912814babb42b864713/greenlet-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:34308836d8370bddadb41f5a7ce96879b72e2fdfb4e87729330c6ab52376409f", size = 231081, upload-time = "2026-02-20T20:17:28.121Z" }, - { url = "https://files.pythonhosted.org/packages/58/2e/fe7f36ff1982d6b10a60d5e0740c759259a7d6d2e1dc41da6d96de32fff6/greenlet-3.3.2-cp312-cp312-win_arm64.whl", hash = "sha256:d3a62fa76a32b462a97198e4c9e99afb9ab375115e74e9a83ce180e7a496f643", size = 230331, upload-time = "2026-02-20T20:17:23.34Z" }, - { url = "https://files.pythonhosted.org/packages/ac/48/f8b875fa7dea7dd9b33245e37f065af59df6a25af2f9561efa8d822fde51/greenlet-3.3.2-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:aa6ac98bdfd716a749b84d4034486863fd81c3abde9aa3cf8eff9127981a4ae4", size = 279120, upload-time = "2026-02-20T20:19:01.9Z" }, - { url = "https://files.pythonhosted.org/packages/49/8d/9771d03e7a8b1ee456511961e1b97a6d77ae1dea4a34a5b98eee706689d3/greenlet-3.3.2-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab0c7e7901a00bc0a7284907273dc165b32e0d109a6713babd04471327ff7986", size = 603238, upload-time = "2026-02-20T20:47:32.873Z" }, - { url = "https://files.pythonhosted.org/packages/59/0e/4223c2bbb63cd5c97f28ffb2a8aee71bdfb30b323c35d409450f51b91e3e/greenlet-3.3.2-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d248d8c23c67d2291ffd47af766e2a3aa9fa1c6703155c099feb11f526c63a92", size = 614219, upload-time = "2026-02-20T20:55:59.817Z" }, - { url = "https://files.pythonhosted.org/packages/94/2b/4d012a69759ac9d77210b8bfb128bc621125f5b20fc398bce3940d036b1c/greenlet-3.3.2-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ccd21bb86944ca9be6d967cf7691e658e43417782bce90b5d2faeda0ff78a7dd", size = 628268, upload-time = "2026-02-20T21:02:48.024Z" }, - { url = "https://files.pythonhosted.org/packages/7a/34/259b28ea7a2a0c904b11cd36c79b8cef8019b26ee5dbe24e73b469dea347/greenlet-3.3.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6997d360a4e6a4e936c0f9625b1c20416b8a0ea18a8e19cabbefc712e7397ab", size = 616774, upload-time = "2026-02-20T20:21:02.454Z" }, - { url = "https://files.pythonhosted.org/packages/0a/03/996c2d1689d486a6e199cb0f1cf9e4aa940c500e01bdf201299d7d61fa69/greenlet-3.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:64970c33a50551c7c50491671265d8954046cb6e8e2999aacdd60e439b70418a", size = 1571277, upload-time = "2026-02-20T20:49:34.795Z" }, - { url = "https://files.pythonhosted.org/packages/d9/c4/2570fc07f34a39f2caf0bf9f24b0a1a0a47bc2e8e465b2c2424821389dfc/greenlet-3.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1a9172f5bf6bd88e6ba5a84e0a68afeac9dc7b6b412b245dd64f52d83c81e55b", size = 1640455, upload-time = "2026-02-20T20:21:10.261Z" }, - { url = "https://files.pythonhosted.org/packages/91/39/5ef5aa23bc545aa0d31e1b9b55822b32c8da93ba657295840b6b34124009/greenlet-3.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:a7945dd0eab63ded0a48e4dcade82939783c172290a7903ebde9e184333ca124", size = 230961, upload-time = "2026-02-20T20:16:58.461Z" }, - { url = "https://files.pythonhosted.org/packages/62/6b/a89f8456dcb06becff288f563618e9f20deed8dd29beea14f9a168aef64b/greenlet-3.3.2-cp313-cp313-win_arm64.whl", hash = "sha256:394ead29063ee3515b4e775216cb756b2e3b4a7e55ae8fd884f17fa579e6b327", size = 230221, upload-time = "2026-02-20T20:17:37.152Z" }, - { url = "https://files.pythonhosted.org/packages/3f/ae/8bffcbd373b57a5992cd077cbe8858fff39110480a9d50697091faea6f39/greenlet-3.3.2-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:8d1658d7291f9859beed69a776c10822a0a799bc4bfe1bd4272bb60e62507dab", size = 279650, upload-time = "2026-02-20T20:18:00.783Z" }, - { url = "https://files.pythonhosted.org/packages/d1/c0/45f93f348fa49abf32ac8439938726c480bd96b2a3c6f4d949ec0124b69f/greenlet-3.3.2-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:18cb1b7337bca281915b3c5d5ae19f4e76d35e1df80f4ad3c1a7be91fadf1082", size = 650295, upload-time = "2026-02-20T20:47:34.036Z" }, - { url = "https://files.pythonhosted.org/packages/b3/de/dd7589b3f2b8372069ab3e4763ea5329940fc7ad9dcd3e272a37516d7c9b/greenlet-3.3.2-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2e47408e8ce1c6f1ceea0dffcdf6ebb85cc09e55c7af407c99f1112016e45e9", size = 662163, upload-time = "2026-02-20T20:56:01.295Z" }, - { url = "https://files.pythonhosted.org/packages/cd/ac/85804f74f1ccea31ba518dcc8ee6f14c79f73fe36fa1beba38930806df09/greenlet-3.3.2-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e3cb43ce200f59483eb82949bf1835a99cf43d7571e900d7c8d5c62cdf25d2f9", size = 675371, upload-time = "2026-02-20T21:02:49.664Z" }, - { url = "https://files.pythonhosted.org/packages/d2/d8/09bfa816572a4d83bccd6750df1926f79158b1c36c5f73786e26dbe4ee38/greenlet-3.3.2-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63d10328839d1973e5ba35e98cccbca71b232b14051fd957b6f8b6e8e80d0506", size = 664160, upload-time = "2026-02-20T20:21:04.015Z" }, - { url = "https://files.pythonhosted.org/packages/48/cf/56832f0c8255d27f6c35d41b5ec91168d74ec721d85f01a12131eec6b93c/greenlet-3.3.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8e4ab3cfb02993c8cc248ea73d7dae6cec0253e9afa311c9b37e603ca9fad2ce", size = 1619181, upload-time = "2026-02-20T20:49:36.052Z" }, - { url = "https://files.pythonhosted.org/packages/0a/23/b90b60a4aabb4cec0796e55f25ffbfb579a907c3898cd2905c8918acaa16/greenlet-3.3.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94ad81f0fd3c0c0681a018a976e5c2bd2ca2d9d94895f23e7bb1af4e8af4e2d5", size = 1687713, upload-time = "2026-02-20T20:21:11.684Z" }, - { url = "https://files.pythonhosted.org/packages/f3/ca/2101ca3d9223a1dc125140dbc063644dca76df6ff356531eb27bc267b446/greenlet-3.3.2-cp314-cp314-win_amd64.whl", hash = "sha256:8c4dd0f3997cf2512f7601563cc90dfb8957c0cff1e3a1b23991d4ea1776c492", size = 232034, upload-time = "2026-02-20T20:20:08.186Z" }, - { url = "https://files.pythonhosted.org/packages/f6/4a/ecf894e962a59dea60f04877eea0fd5724618da89f1867b28ee8b91e811f/greenlet-3.3.2-cp314-cp314-win_arm64.whl", hash = "sha256:cd6f9e2bbd46321ba3bbb4c8a15794d32960e3b0ae2cc4d49a1a53d314805d71", size = 231437, upload-time = "2026-02-20T20:18:59.722Z" }, - { url = "https://files.pythonhosted.org/packages/98/6d/8f2ef704e614bcf58ed43cfb8d87afa1c285e98194ab2cfad351bf04f81e/greenlet-3.3.2-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:e26e72bec7ab387ac80caa7496e0f908ff954f31065b0ffc1f8ecb1338b11b54", size = 286617, upload-time = "2026-02-20T20:19:29.856Z" }, - { url = "https://files.pythonhosted.org/packages/5e/0d/93894161d307c6ea237a43988f27eba0947b360b99ac5239ad3fe09f0b47/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b466dff7a4ffda6ca975979bab80bdadde979e29fc947ac3be4451428d8b0e4", size = 655189, upload-time = "2026-02-20T20:47:35.742Z" }, - { url = "https://files.pythonhosted.org/packages/f5/2c/d2d506ebd8abcb57386ec4f7ba20f4030cbe56eae541bc6fd6ef399c0b41/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b8bddc5b73c9720bea487b3bffdb1840fe4e3656fba3bd40aa1489e9f37877ff", size = 658225, upload-time = "2026-02-20T20:56:02.527Z" }, - { url = "https://files.pythonhosted.org/packages/d1/67/8197b7e7e602150938049d8e7f30de1660cfb87e4c8ee349b42b67bdb2e1/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:59b3e2c40f6706b05a9cd299c836c6aa2378cabe25d021acd80f13abf81181cf", size = 666581, upload-time = "2026-02-20T21:02:51.526Z" }, - { url = "https://files.pythonhosted.org/packages/8e/30/3a09155fbf728673a1dea713572d2d31159f824a37c22da82127056c44e4/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b26b0f4428b871a751968285a1ac9648944cea09807177ac639b030bddebcea4", size = 657907, upload-time = "2026-02-20T20:21:05.259Z" }, - { url = "https://files.pythonhosted.org/packages/f3/fd/d05a4b7acd0154ed758797f0a43b4c0962a843bedfe980115e842c5b2d08/greenlet-3.3.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1fb39a11ee2e4d94be9a76671482be9398560955c9e568550de0224e41104727", size = 1618857, upload-time = "2026-02-20T20:49:37.309Z" }, - { url = "https://files.pythonhosted.org/packages/6f/e1/50ee92a5db521de8f35075b5eff060dd43d39ebd46c2181a2042f7070385/greenlet-3.3.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:20154044d9085151bc309e7689d6f7ba10027f8f5a8c0676ad398b951913d89e", size = 1680010, upload-time = "2026-02-20T20:21:13.427Z" }, - { url = "https://files.pythonhosted.org/packages/29/4b/45d90626aef8e65336bed690106d1382f7a43665e2249017e9527df8823b/greenlet-3.3.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c04c5e06ec3e022cbfe2cd4a846e1d4e50087444f875ff6d2c2ad8445495cf1a", size = 237086, upload-time = "2026-02-20T20:20:45.786Z" }, +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/86/94/a5935717b307d7c71fe877b52b884c6af707d2d2090db118a03fbd799369/greenlet-3.4.0.tar.gz", hash = "sha256:f50a96b64dafd6169e595a5c56c9146ef80333e67d4476a65a9c55f400fc22ff", size = 195913, upload-time = "2026-04-08T17:08:00.863Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/c6/dba32cab7e3a625b011aa5647486e2d28423a48845a2998c126dd69c85e1/greenlet-3.4.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:805bebb4945094acbab757d34d6e1098be6de8966009ab9ca54f06ff492def58", size = 285504, upload-time = "2026-04-08T15:52:14.071Z" }, + { url = "https://files.pythonhosted.org/packages/54/f4/7cb5c2b1feb9a1f50e038be79980dfa969aa91979e5e3a18fdbcfad2c517/greenlet-3.4.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:439fc2f12b9b512d9dfa681c5afe5f6b3232c708d13e6f02c845e0d9f4c2d8c6", size = 605476, upload-time = "2026-04-08T16:24:37.064Z" }, + { url = "https://files.pythonhosted.org/packages/d6/af/b66ab0b2f9a4c5a867c136bf66d9599f34f21a1bcca26a2884a29c450bd9/greenlet-3.4.0-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a70ed1cb0295bee1df57b63bf7f46b4e56a5c93709eea769c1fec1bb23a95875", size = 618336, upload-time = "2026-04-08T16:30:56.59Z" }, + { url = "https://files.pythonhosted.org/packages/6d/31/56c43d2b5de476f77d36ceeec436328533bff960a4cba9a07616e93063ab/greenlet-3.4.0-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8c5696c42e6bb5cfb7c6ff4453789081c66b9b91f061e5e9367fa15792644e76", size = 625045, upload-time = "2026-04-08T16:40:37.111Z" }, + { url = "https://files.pythonhosted.org/packages/e5/5c/8c5633ece6ba611d64bf2770219a98dd439921d6424e4e8cf16b0ac74ea5/greenlet-3.4.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c660bce1940a1acae5f51f0a064f1bc785d07ea16efcb4bc708090afc4d69e83", size = 613515, upload-time = "2026-04-08T15:56:32.478Z" }, + { url = "https://files.pythonhosted.org/packages/80/ca/704d4e2c90acb8bdf7ae593f5cbc95f58e82de95cc540fb75631c1054533/greenlet-3.4.0-cp311-cp311-manylinux_2_39_riscv64.whl", hash = "sha256:89995ce5ddcd2896d89615116dd39b9703bfa0c07b583b85b89bf1b5d6eddf81", size = 419745, upload-time = "2026-04-08T16:43:04.022Z" }, + { url = "https://files.pythonhosted.org/packages/a9/df/950d15bca0d90a0e7395eb777903060504cdb509b7b705631e8fb69ff415/greenlet-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ee407d4d1ca9dc632265aee1c8732c4a2d60adff848057cdebfe5fe94eb2c8a2", size = 1574623, upload-time = "2026-04-08T16:26:18.596Z" }, + { url = "https://files.pythonhosted.org/packages/1a/e7/0839afab829fcb7333c9ff6d80c040949510055d2d4d63251f0d1c7c804e/greenlet-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:956215d5e355fffa7c021d168728321fd4d31fd730ac609b1653b450f6a4bc71", size = 1639579, upload-time = "2026-04-08T15:57:29.231Z" }, + { url = "https://files.pythonhosted.org/packages/d9/2b/b4482401e9bcaf9f5c97f67ead38db89c19520ff6d0d6699979c6efcc200/greenlet-3.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:5cb614ace7c27571270354e9c9f696554d073f8aa9319079dcba466bbdead711", size = 238233, upload-time = "2026-04-08T17:02:54.286Z" }, + { url = "https://files.pythonhosted.org/packages/0c/4d/d8123a4e0bcd583d5cfc8ddae0bbe29c67aab96711be331a7cc935a35966/greenlet-3.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:04403ac74fe295a361f650818de93be11b5038a78f49ccfb64d3b1be8fbf1267", size = 235045, upload-time = "2026-04-08T17:04:05.072Z" }, + { url = "https://files.pythonhosted.org/packages/65/8b/3669ad3b3f247a791b2b4aceb3aa5a31f5f6817bf547e4e1ff712338145a/greenlet-3.4.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:1a54a921561dd9518d31d2d3db4d7f80e589083063ab4d3e2e950756ef809e1a", size = 286902, upload-time = "2026-04-08T15:52:12.138Z" }, + { url = "https://files.pythonhosted.org/packages/38/3e/3c0e19b82900873e2d8469b590a6c4b3dfd2b316d0591f1c26b38a4879a5/greenlet-3.4.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:16dec271460a9a2b154e3b1c2fa1050ce6280878430320e85e08c166772e3f97", size = 606099, upload-time = "2026-04-08T16:24:38.408Z" }, + { url = "https://files.pythonhosted.org/packages/b5/33/99fef65e7754fc76a4ed14794074c38c9ed3394a5bd129d7f61b705f3168/greenlet-3.4.0-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:90036ce224ed6fe75508c1907a77e4540176dcf0744473627785dd519c6f9996", size = 618837, upload-time = "2026-04-08T16:30:58.298Z" }, + { url = "https://files.pythonhosted.org/packages/44/57/eae2cac10421feae6c0987e3dc106c6d86262b1cb379e171b017aba893a6/greenlet-3.4.0-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6f0def07ec9a71d72315cf26c061aceee53b306c36ed38c35caba952ea1b319d", size = 624901, upload-time = "2026-04-08T16:40:38.981Z" }, + { url = "https://files.pythonhosted.org/packages/36/f7/229f3aed6948faa20e0616a0b8568da22e365ede6a54d7d369058b128afd/greenlet-3.4.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a1c4f6b453006efb8310affb2d132832e9bbb4fc01ce6df6b70d810d38f1f6dc", size = 615062, upload-time = "2026-04-08T15:56:33.766Z" }, + { url = "https://files.pythonhosted.org/packages/6a/8a/0e73c9b94f31d1cc257fe79a0eff621674141cdae7d6d00f40de378a1e42/greenlet-3.4.0-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:0e1254cf0cbaa17b04320c3a78575f29f3c161ef38f59c977108f19ffddaf077", size = 423927, upload-time = "2026-04-08T16:43:05.293Z" }, + { url = "https://files.pythonhosted.org/packages/08/97/d988180011aa40135c46cd0d0cf01dd97f7162bae14139b4a3ef54889ba5/greenlet-3.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9b2d9a138ffa0e306d0e2b72976d2fb10b97e690d40ab36a472acaab0838e2de", size = 1573511, upload-time = "2026-04-08T16:26:20.058Z" }, + { url = "https://files.pythonhosted.org/packages/d4/0f/a5a26fe152fb3d12e6a474181f6e9848283504d0afd095f353d85726374b/greenlet-3.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8424683caf46eb0eb6f626cb95e008e8cc30d0cb675bdfa48200925c79b38a08", size = 1640396, upload-time = "2026-04-08T15:57:30.88Z" }, + { url = "https://files.pythonhosted.org/packages/42/cf/bb2c32d9a100e36ee9f6e38fad6b1e082b8184010cb06259b49e1266ca01/greenlet-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:a0a53fb071531d003b075c444014ff8f8b1a9898d36bb88abd9ac7b3524648a2", size = 238892, upload-time = "2026-04-08T17:03:10.094Z" }, + { url = "https://files.pythonhosted.org/packages/b7/47/6c41314bac56e71436ce551c7fbe3cc830ed857e6aa9708dbb9c65142eb6/greenlet-3.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:f38b81880ba28f232f1f675893a39cf7b6db25b31cc0a09bb50787ecf957e85e", size = 235599, upload-time = "2026-04-08T15:52:54.3Z" }, + { url = "https://files.pythonhosted.org/packages/7a/75/7e9cd1126a1e1f0cd67b0eda02e5221b28488d352684704a78ed505bd719/greenlet-3.4.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:43748988b097f9c6f09364f260741aa73c80747f63389824435c7a50bfdfd5c1", size = 285856, upload-time = "2026-04-08T15:52:45.82Z" }, + { url = "https://files.pythonhosted.org/packages/9d/c4/3e2df392e5cb199527c4d9dbcaa75c14edcc394b45040f0189f649631e3c/greenlet-3.4.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5566e4e2cd7a880e8c27618e3eab20f3494452d12fd5129edef7b2f7aa9a36d1", size = 610208, upload-time = "2026-04-08T16:24:39.674Z" }, + { url = "https://files.pythonhosted.org/packages/da/af/750cdfda1d1bd30a6c28080245be8d0346e669a98fdbae7f4102aa95fff3/greenlet-3.4.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1054c5a3c78e2ab599d452f23f7adafef55062a783a8e241d24f3b633ba6ff82", size = 621269, upload-time = "2026-04-08T16:30:59.767Z" }, + { url = "https://files.pythonhosted.org/packages/e0/93/c8c508d68ba93232784bbc1b5474d92371f2897dfc6bc281b419f2e0d492/greenlet-3.4.0-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:98eedd1803353daf1cd9ef23eef23eda5a4d22f99b1f998d273a8b78b70dd47f", size = 628455, upload-time = "2026-04-08T16:40:40.698Z" }, + { url = "https://files.pythonhosted.org/packages/54/78/0cbc693622cd54ebe25207efbb3a0eb07c2639cb8594f6e3aaaa0bb077a8/greenlet-3.4.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f82cb6cddc27dd81c96b1506f4aa7def15070c3b2a67d4e46fd19016aacce6cf", size = 617549, upload-time = "2026-04-08T15:56:34.893Z" }, + { url = "https://files.pythonhosted.org/packages/7f/46/cfaaa0ade435a60550fd83d07dfd5c41f873a01da17ede5c4cade0b9bab8/greenlet-3.4.0-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:b7857e2202aae67bc5725e0c1f6403c20a8ff46094ece015e7d474f5f7020b55", size = 426238, upload-time = "2026-04-08T16:43:06.865Z" }, + { url = "https://files.pythonhosted.org/packages/ba/c0/8966767de01343c1ff47e8b855dc78e7d1a8ed2b7b9c83576a57e289f81d/greenlet-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:227a46251ecba4ff46ae742bc5ce95c91d5aceb4b02f885487aff269c127a729", size = 1575310, upload-time = "2026-04-08T16:26:21.671Z" }, + { url = "https://files.pythonhosted.org/packages/b8/38/bcdc71ba05e9a5fda87f63ffc2abcd1f15693b659346df994a48c968003d/greenlet-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5b99e87be7eba788dd5b75ba1cde5639edffdec5f91fe0d734a249535ec3408c", size = 1640435, upload-time = "2026-04-08T15:57:32.572Z" }, + { url = "https://files.pythonhosted.org/packages/a1/c2/19b664b7173b9e4ef5f77e8cef9f14c20ec7fce7920dc1ccd7afd955d093/greenlet-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:849f8bc17acd6295fcb5de8e46d55cc0e52381c56eaf50a2afd258e97bc65940", size = 238760, upload-time = "2026-04-08T17:04:03.878Z" }, + { url = "https://files.pythonhosted.org/packages/9b/96/795619651d39c7fbd809a522f881aa6f0ead504cc8201c3a5b789dfaef99/greenlet-3.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:9390ad88b652b1903814eaabd629ca184db15e0eeb6fe8a390bbf8b9106ae15a", size = 235498, upload-time = "2026-04-08T17:05:00.584Z" }, + { url = "https://files.pythonhosted.org/packages/78/02/bde66806e8f169cf90b14d02c500c44cdbe02c8e224c9c67bafd1b8cadd1/greenlet-3.4.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:10a07aca6babdd18c16a3f4f8880acfffc2b88dfe431ad6aa5f5740759d7d75e", size = 286291, upload-time = "2026-04-08T17:09:34.307Z" }, + { url = "https://files.pythonhosted.org/packages/05/1f/39da1c336a87d47c58352fb8a78541ce63d63ae57c5b9dae1fe02801bbc2/greenlet-3.4.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:076e21040b3a917d3ce4ad68fb5c3c6b32f1405616c4a57aa83120979649bd3d", size = 656749, upload-time = "2026-04-08T16:24:41.721Z" }, + { url = "https://files.pythonhosted.org/packages/d3/6c/90ee29a4ee27af7aa2e2ec408799eeb69ee3fcc5abcecac6ddd07a5cd0f2/greenlet-3.4.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e82689eea4a237e530bb5cb41b180ef81fa2160e1f89422a67be7d90da67f615", size = 669084, upload-time = "2026-04-08T16:31:01.372Z" }, + { url = "https://files.pythonhosted.org/packages/d2/4a/74078d3936712cff6d3c91a930016f476ce4198d84e224fe6d81d3e02880/greenlet-3.4.0-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:06c2d3b89e0c62ba50bd7adf491b14f39da9e7e701647cb7b9ff4c99bee04b19", size = 673405, upload-time = "2026-04-08T16:40:42.527Z" }, + { url = "https://files.pythonhosted.org/packages/07/49/d4cad6e5381a50947bb973d2f6cf6592621451b09368b8c20d9b8af49c5b/greenlet-3.4.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4df3b0b2289ec686d3c821a5fee44259c05cfe824dd5e6e12c8e5f5df23085cf", size = 665621, upload-time = "2026-04-08T15:56:35.995Z" }, + { url = "https://files.pythonhosted.org/packages/79/3e/df8a83ab894751bc31e1106fdfaa80ca9753222f106b04de93faaa55feb7/greenlet-3.4.0-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:070b8bac2ff3b4d9e0ff36a0d19e42103331d9737e8504747cd1e659f76297bd", size = 471670, upload-time = "2026-04-08T16:43:08.512Z" }, + { url = "https://files.pythonhosted.org/packages/37/31/d1edd54f424761b5d47718822f506b435b6aab2f3f93b465441143ea5119/greenlet-3.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8bff29d586ea415688f4cec96a591fcc3bf762d046a796cdadc1fdb6e7f2d5bf", size = 1622259, upload-time = "2026-04-08T16:26:23.201Z" }, + { url = "https://files.pythonhosted.org/packages/b0/c6/6d3f9cdcb21c4e12a79cb332579f1c6aa1af78eb68059c5a957c7812d95e/greenlet-3.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8a569c2fb840c53c13a2b8967c63621fafbd1a0e015b9c82f408c33d626a2fda", size = 1686916, upload-time = "2026-04-08T15:57:34.282Z" }, + { url = "https://files.pythonhosted.org/packages/63/45/c1ca4a1ad975de4727e52d3ffe641ae23e1d7a8ffaa8ff7a0477e1827b92/greenlet-3.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:207ba5b97ea8b0b60eb43ffcacf26969dd83726095161d676aac03ff913ee50d", size = 239821, upload-time = "2026-04-08T17:03:48.423Z" }, + { url = "https://files.pythonhosted.org/packages/71/c4/6f621023364d7e85a4769c014c8982f98053246d142420e0328980933ceb/greenlet-3.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:f8296d4e2b92af34ebde81085a01690f26a51eb9ac09a0fcadb331eb36dbc802", size = 236932, upload-time = "2026-04-08T17:04:33.551Z" }, + { url = "https://files.pythonhosted.org/packages/d4/8f/18d72b629783f5e8d045a76f5325c1e938e659a9e4da79c7dcd10169a48d/greenlet-3.4.0-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:d70012e51df2dbbccfaf63a40aaf9b40c8bed37c3e3a38751c926301ce538ece", size = 294681, upload-time = "2026-04-08T15:52:35.778Z" }, + { url = "https://files.pythonhosted.org/packages/9e/ad/5fa86ec46769c4153820d58a04062285b3b9e10ba3d461ee257b68dcbf53/greenlet-3.4.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a58bec0751f43068cd40cff31bb3ca02ad6000b3a51ca81367af4eb5abc480c8", size = 658899, upload-time = "2026-04-08T16:24:43.32Z" }, + { url = "https://files.pythonhosted.org/packages/43/f0/4e8174ca0e87ae748c409f055a1ba161038c43cc0a5a6f1433a26ac2e5bf/greenlet-3.4.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:05fa0803561028f4b2e3b490ee41216a842eaee11aed004cc343a996d9523aa2", size = 665284, upload-time = "2026-04-08T16:31:02.833Z" }, + { url = "https://files.pythonhosted.org/packages/ef/92/466b0d9afd44b8af623139a3599d651c7564fa4152f25f117e1ee5949ffb/greenlet-3.4.0-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c4cd56a9eb7a6444edbc19062f7b6fbc8f287c663b946e3171d899693b1c19fa", size = 665872, upload-time = "2026-04-08T16:40:43.912Z" }, + { url = "https://files.pythonhosted.org/packages/19/da/991cf7cd33662e2df92a1274b7eb4d61769294d38a1bba8a45f31364845e/greenlet-3.4.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e60d38719cb80b3ab5e85f9f1aed4960acfde09868af6762ccb27b260d68f4ed", size = 661861, upload-time = "2026-04-08T15:56:37.269Z" }, + { url = "https://files.pythonhosted.org/packages/0d/14/3395a7ef3e260de0325152ddfe19dffb3e49fe10873b94654352b53ad48e/greenlet-3.4.0-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:1f85f204c4d54134ae850d401fa435c89cd667d5ce9dc567571776b45941af72", size = 489237, upload-time = "2026-04-08T16:43:09.993Z" }, + { url = "https://files.pythonhosted.org/packages/36/c5/6c2c708e14db3d9caea4b459d8464f58c32047451142fe2cfd90e7458f41/greenlet-3.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7f50c804733b43eded05ae694691c9aa68bca7d0a867d67d4a3f514742a2d53f", size = 1622182, upload-time = "2026-04-08T16:26:24.777Z" }, + { url = "https://files.pythonhosted.org/packages/7a/4c/50c5fed19378e11a29fabab1f6be39ea95358f4a0a07e115a51ca93385d8/greenlet-3.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2d4f0635dc4aa638cda4b2f5a07ae9a2cff9280327b581a3fcb6f317b4fbc38a", size = 1685050, upload-time = "2026-04-08T15:57:36.453Z" }, + { url = "https://files.pythonhosted.org/packages/db/72/85ae954d734703ab48e622c59d4ce35d77ce840c265814af9c078cacc7aa/greenlet-3.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:1a4a48f24681300c640f143ba7c404270e1ebbbcf34331d7104a4ff40f8ea705", size = 245554, upload-time = "2026-04-08T17:03:50.044Z" }, ] [[package]] @@ -759,7 +785,8 @@ dependencies = [ { name = "appnope", marker = "sys_platform == 'darwin'" }, { name = "comm" }, { name = "debugpy" }, - { name = "ipython" }, + { name = "ipython", version = "9.10.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "ipython", version = "9.12.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "jupyter-client" }, { name = "jupyter-core" }, { name = "matplotlib-inline" }, @@ -777,24 +804,52 @@ wheels = [ [[package]] name = "ipython" -version = "9.10.0" +version = "9.10.1" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.12'", +] dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "decorator" }, - { name = "ipython-pygments-lexers" }, - { name = "jedi" }, - { name = "matplotlib-inline" }, - { name = "pexpect", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, - { name = "prompt-toolkit" }, - { name = "pygments" }, - { name = "stack-data" }, - { name = "traitlets" }, + { name = "colorama", marker = "python_full_version < '3.12' and sys_platform == 'win32'" }, + { name = "decorator", marker = "python_full_version < '3.12'" }, + { name = "ipython-pygments-lexers", marker = "python_full_version < '3.12'" }, + { name = "jedi", marker = "python_full_version < '3.12'" }, + { name = "matplotlib-inline", marker = "python_full_version < '3.12'" }, + { name = "pexpect", marker = "python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "prompt-toolkit", marker = "python_full_version < '3.12'" }, + { name = "pygments", marker = "python_full_version < '3.12'" }, + { name = "stack-data", marker = "python_full_version < '3.12'" }, + { name = "traitlets", marker = "python_full_version < '3.12'" }, { name = "typing-extensions", marker = "python_full_version < '3.12'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a6/60/2111715ea11f39b1535bed6024b7dec7918b71e5e5d30855a5b503056b50/ipython-9.10.0.tar.gz", hash = "sha256:cd9e656be97618a0676d058134cd44e6dc7012c0e5cb36a9ce96a8c904adaf77", size = 4426526, upload-time = "2026-02-02T10:00:33.594Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c5/25/daae0e764047b0a2480c7bbb25d48f4f509b5818636562eeac145d06dfee/ipython-9.10.1.tar.gz", hash = "sha256:e170e9b2a44312484415bdb750492699bf329233b03f2557a9692cce6466ada4", size = 4426663, upload-time = "2026-03-27T09:53:26.244Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3d/aa/898dec789a05731cd5a9f50605b7b44a72bd198fd0d4528e11fc610177cc/ipython-9.10.0-py3-none-any.whl", hash = "sha256:c6ab68cc23bba8c7e18e9b932797014cc61ea7fd6f19de180ab9ba73e65ee58d", size = 622774, upload-time = "2026-02-02T10:00:31.503Z" }, + { url = "https://files.pythonhosted.org/packages/01/09/ba70f8d662d5671687da55ad2cc0064cf795b15e1eea70907532202e7c97/ipython-9.10.1-py3-none-any.whl", hash = "sha256:82d18ae9fb9164ded080c71ef92a182ee35ee7db2395f67616034bebb020a232", size = 622827, upload-time = "2026-03-27T09:53:24.566Z" }, +] + +[[package]] +name = "ipython" +version = "9.12.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.12' and python_full_version < '3.14'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version >= '3.12' and sys_platform == 'win32'" }, + { name = "decorator", marker = "python_full_version >= '3.12'" }, + { name = "ipython-pygments-lexers", marker = "python_full_version >= '3.12'" }, + { name = "jedi", marker = "python_full_version >= '3.12'" }, + { name = "matplotlib-inline", marker = "python_full_version >= '3.12'" }, + { name = "pexpect", marker = "python_full_version >= '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "prompt-toolkit", marker = "python_full_version >= '3.12'" }, + { name = "pygments", marker = "python_full_version >= '3.12'" }, + { name = "stack-data", marker = "python_full_version >= '3.12'" }, + { name = "traitlets", marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3a/73/7114f80a8f9cabdb13c27732dce24af945b2923dcab80723602f7c8bc2d8/ipython-9.12.0.tar.gz", hash = "sha256:01daa83f504b693ba523b5a407246cabde4eb4513285a3c6acaff11a66735ee4", size = 4428879, upload-time = "2026-03-27T09:42:45.312Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/22/906c8108974c673ebef6356c506cebb6870d48cedea3c41e949e2dd556bb/ipython-9.12.0-py3-none-any.whl", hash = "sha256:0f2701e8ee86e117e37f50563205d36feaa259d2e08d4a6bc6b6d74b18ce128d", size = 625661, upload-time = "2026-03-27T09:42:42.831Z" }, ] [[package]] @@ -847,20 +902,20 @@ wheels = [ [[package]] name = "json5" -version = "0.13.0" +version = "0.14.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/77/e8/a3f261a66e4663f22700bc8a17c08cb83e91fbf086726e7a228398968981/json5-0.13.0.tar.gz", hash = "sha256:b1edf8d487721c0bf64d83c28e91280781f6e21f4a797d3261c7c828d4c165bf", size = 52441, upload-time = "2026-01-01T19:42:14.99Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9c/4b/6f8906aaf67d501e259b0adab4d312945bb7211e8b8d4dcc77c92320edaa/json5-0.14.0.tar.gz", hash = "sha256:b3f492fad9f6cdbced8b7d40b28b9b1c9701c5f561bef0d33b81c2ff433fefcb", size = 52656, upload-time = "2026-03-27T22:50:48.108Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d7/9e/038522f50ceb7e74f1f991bf1b699f24b0c2bbe7c390dd36ad69f4582258/json5-0.13.0-py3-none-any.whl", hash = "sha256:9a08e1dd65f6a4d4c6fa82d216cf2477349ec2346a38fd70cc11d2557499fbcc", size = 36163, upload-time = "2026-01-01T19:42:13.962Z" }, + { url = "https://files.pythonhosted.org/packages/b8/42/cf027b4ac873b076189d935b135397675dac80cb29acb13e1ab86ad6c631/json5-0.14.0-py3-none-any.whl", hash = "sha256:56cf861bab076b1178eb8c92e1311d273a9b9acea2ccc82c276abf839ebaef3a", size = 36271, upload-time = "2026-03-27T22:50:47.073Z" }, ] [[package]] name = "jsonpointer" -version = "3.0.0" +version = "3.1.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6a/0a/eebeb1fa92507ea94016a2a790b93c2ae41a7e18778f85471dc54475ed25/jsonpointer-3.0.0.tar.gz", hash = "sha256:2b2d729f2091522d61c3b31f82e11870f60b68f43fbc705cb76bf4b832af59ef", size = 9114, upload-time = "2024-06-10T19:24:42.462Z" } +sdist = { url = "https://files.pythonhosted.org/packages/18/c7/af399a2e7a67fd18d63c40c5e62d3af4e67b836a2107468b6a5ea24c4304/jsonpointer-3.1.1.tar.gz", hash = "sha256:0b801c7db33a904024f6004d526dcc53bbb8a4a0f4e32bfd10beadf60adf1900", size = 9068, upload-time = "2026-03-23T22:32:32.458Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/71/92/5e77f98553e9e75130c78900d000368476aed74276eb8ae8796f65f00918/jsonpointer-3.0.0-py2.py3-none-any.whl", hash = "sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942", size = 7595, upload-time = "2024-06-10T19:24:40.698Z" }, + { url = "https://files.pythonhosted.org/packages/9e/6a/a83720e953b1682d2d109d3c2dbb0bc9bf28cc1cbc205be4ef4be5da709d/jsonpointer-3.1.1-py3-none-any.whl", hash = "sha256:8ff8b95779d071ba472cf5bc913028df06031797532f08a7d5b602d8b2a488ca", size = 7659, upload-time = "2026-03-23T22:32:31.568Z" }, ] [[package]] @@ -953,14 +1008,14 @@ wheels = [ [[package]] name = "jupyter-lsp" -version = "2.3.0" +version = "2.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jupyter-server" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/eb/5a/9066c9f8e94ee517133cd98dba393459a16cd48bba71a82f16a65415206c/jupyter_lsp-2.3.0.tar.gz", hash = "sha256:458aa59339dc868fb784d73364f17dbce8836e906cd75fd471a325cba02e0245", size = 54823, upload-time = "2025-08-27T17:47:34.671Z" } +sdist = { url = "https://files.pythonhosted.org/packages/36/ff/1e4a61f5170a9a1d978f3ac3872449de6c01fc71eaf89657824c878b1549/jupyter_lsp-2.3.1.tar.gz", hash = "sha256:fdf8a4aa7d85813976d6e29e95e6a2c8f752701f926f2715305249a3829805a6", size = 55677, upload-time = "2026-04-02T08:10:06.749Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1a/60/1f6cee0c46263de1173894f0fafcb3475ded276c472c14d25e0280c18d6d/jupyter_lsp-2.3.0-py3-none-any.whl", hash = "sha256:e914a3cb2addf48b1c7710914771aaf1819d46b2e5a79b0f917b5478ec93f34f", size = 76687, upload-time = "2025-08-27T17:47:33.15Z" }, + { url = "https://files.pythonhosted.org/packages/23/e8/9d61dcbd1dce8ef418f06befd4ac084b4720429c26b0b1222bc218685eff/jupyter_lsp-2.3.1-py3-none-any.whl", hash = "sha256:71b954d834e85ff3096400554f2eefaf7fe37053036f9a782b0f7c5e42dadb81", size = 77513, upload-time = "2026-04-02T08:10:01.753Z" }, ] [[package]] @@ -1008,7 +1063,7 @@ wheels = [ [[package]] name = "jupyterlab" -version = "4.5.5" +version = "4.5.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "async-lru" }, @@ -1025,9 +1080,9 @@ dependencies = [ { name = "tornado" }, { name = "traitlets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6e/2d/953a5612a34a3c799a62566a548e711d103f631672fd49650e0f2de80870/jupyterlab-4.5.5.tar.gz", hash = "sha256:eac620698c59eb810e1729909be418d9373d18137cac66637141abba613b3fda", size = 23968441, upload-time = "2026-02-23T18:57:34.339Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ac/d5/730628e03fff2e8a8e8ccdaedde1489ab1309f9a4fa2536248884e30b7c7/jupyterlab-4.5.6.tar.gz", hash = "sha256:642fe2cfe7f0f5922a8a558ba7a0d246c7bc133b708dfe43f7b3a826d163cf42", size = 23970670, upload-time = "2026-03-11T14:17:04.531Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b9/52/372d3494766d690dfdd286871bf5f7fb9a6c61f7566ccaa7153a163dd1df/jupyterlab-4.5.5-py3-none-any.whl", hash = "sha256:a35694a40a8e7f2e82f387472af24e61b22adcce87b5a8ab97a5d9c486202a6d", size = 12446824, upload-time = "2026-02-23T18:57:30.398Z" }, + { url = "https://files.pythonhosted.org/packages/e1/1b/dad6fdcc658ed7af26fdf3841e7394072c9549a8b896c381ab49dd11e2d9/jupyterlab-4.5.6-py3-none-any.whl", hash = "sha256:d6b3dac883aa4d9993348e0f8e95b24624f75099aed64eab6a4351a9cdd1e580", size = 12447124, upload-time = "2026-03-11T14:17:00.229Z" }, ] [[package]] @@ -1068,75 +1123,75 @@ wheels = [ [[package]] name = "librt" -version = "0.8.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/56/9c/b4b0c54d84da4a94b37bd44151e46d5e583c9534c7e02250b961b1b6d8a8/librt-0.8.1.tar.gz", hash = "sha256:be46a14693955b3bd96014ccbdb8339ee8c9346fbe11c1b78901b55125f14c73", size = 177471, upload-time = "2026-02-17T16:13:06.101Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1d/01/0e748af5e4fee180cf7cd12bd12b0513ad23b045dccb2a83191bde82d168/librt-0.8.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:681dc2451d6d846794a828c16c22dc452d924e9f700a485b7ecb887a30aad1fd", size = 65315, upload-time = "2026-02-17T16:11:25.152Z" }, - { url = "https://files.pythonhosted.org/packages/9d/4d/7184806efda571887c798d573ca4134c80ac8642dcdd32f12c31b939c595/librt-0.8.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3b4350b13cc0e6f5bec8fa7caf29a8fb8cdc051a3bae45cfbfd7ce64f009965", size = 68021, upload-time = "2026-02-17T16:11:26.129Z" }, - { url = "https://files.pythonhosted.org/packages/ae/88/c3c52d2a5d5101f28d3dc89298444626e7874aa904eed498464c2af17627/librt-0.8.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ac1e7817fd0ed3d14fd7c5df91daed84c48e4c2a11ee99c0547f9f62fdae13da", size = 194500, upload-time = "2026-02-17T16:11:27.177Z" }, - { url = "https://files.pythonhosted.org/packages/d6/5d/6fb0a25b6a8906e85b2c3b87bee1d6ed31510be7605b06772f9374ca5cb3/librt-0.8.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:747328be0c5b7075cde86a0e09d7a9196029800ba75a1689332348e998fb85c0", size = 205622, upload-time = "2026-02-17T16:11:28.242Z" }, - { url = "https://files.pythonhosted.org/packages/b2/a6/8006ae81227105476a45691f5831499e4d936b1c049b0c1feb17c11b02d1/librt-0.8.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f0af2bd2bc204fa27f3d6711d0f360e6b8c684a035206257a81673ab924aa11e", size = 218304, upload-time = "2026-02-17T16:11:29.344Z" }, - { url = "https://files.pythonhosted.org/packages/ee/19/60e07886ad16670aae57ef44dada41912c90906a6fe9f2b9abac21374748/librt-0.8.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d480de377f5b687b6b1bc0c0407426da556e2a757633cc7e4d2e1a057aa688f3", size = 211493, upload-time = "2026-02-17T16:11:30.445Z" }, - { url = "https://files.pythonhosted.org/packages/9c/cf/f666c89d0e861d05600438213feeb818c7514d3315bae3648b1fc145d2b6/librt-0.8.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d0ee06b5b5291f609ddb37b9750985b27bc567791bc87c76a569b3feed8481ac", size = 219129, upload-time = "2026-02-17T16:11:32.021Z" }, - { url = "https://files.pythonhosted.org/packages/8f/ef/f1bea01e40b4a879364c031476c82a0dc69ce068daad67ab96302fed2d45/librt-0.8.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9e2c6f77b9ad48ce5603b83b7da9ee3e36b3ab425353f695cba13200c5d96596", size = 213113, upload-time = "2026-02-17T16:11:33.192Z" }, - { url = "https://files.pythonhosted.org/packages/9b/80/cdab544370cc6bc1b72ea369525f547a59e6938ef6863a11ab3cd24759af/librt-0.8.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:439352ba9373f11cb8e1933da194dcc6206daf779ff8df0ed69c5e39113e6a99", size = 212269, upload-time = "2026-02-17T16:11:34.373Z" }, - { url = "https://files.pythonhosted.org/packages/9d/9c/48d6ed8dac595654f15eceab2035131c136d1ae9a1e3548e777bb6dbb95d/librt-0.8.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:82210adabbc331dbb65d7868b105185464ef13f56f7f76688565ad79f648b0fe", size = 234673, upload-time = "2026-02-17T16:11:36.063Z" }, - { url = "https://files.pythonhosted.org/packages/16/01/35b68b1db517f27a01be4467593292eb5315def8900afad29fabf56304ba/librt-0.8.1-cp311-cp311-win32.whl", hash = "sha256:52c224e14614b750c0a6d97368e16804a98c684657c7518752c356834fff83bb", size = 54597, upload-time = "2026-02-17T16:11:37.544Z" }, - { url = "https://files.pythonhosted.org/packages/71/02/796fe8f02822235966693f257bf2c79f40e11337337a657a8cfebba5febc/librt-0.8.1-cp311-cp311-win_amd64.whl", hash = "sha256:c00e5c884f528c9932d278d5c9cbbea38a6b81eb62c02e06ae53751a83a4d52b", size = 61733, upload-time = "2026-02-17T16:11:38.691Z" }, - { url = "https://files.pythonhosted.org/packages/28/ad/232e13d61f879a42a4e7117d65e4984bb28371a34bb6fb9ca54ec2c8f54e/librt-0.8.1-cp311-cp311-win_arm64.whl", hash = "sha256:f7cdf7f26c2286ffb02e46d7bac56c94655540b26347673bea15fa52a6af17e9", size = 52273, upload-time = "2026-02-17T16:11:40.308Z" }, - { url = "https://files.pythonhosted.org/packages/95/21/d39b0a87ac52fc98f621fb6f8060efb017a767ebbbac2f99fbcbc9ddc0d7/librt-0.8.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a28f2612ab566b17f3698b0da021ff9960610301607c9a5e8eaca62f5e1c350a", size = 66516, upload-time = "2026-02-17T16:11:41.604Z" }, - { url = "https://files.pythonhosted.org/packages/69/f1/46375e71441c43e8ae335905e069f1c54febee63a146278bcee8782c84fd/librt-0.8.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:60a78b694c9aee2a0f1aaeaa7d101cf713e92e8423a941d2897f4fa37908dab9", size = 68634, upload-time = "2026-02-17T16:11:43.268Z" }, - { url = "https://files.pythonhosted.org/packages/0a/33/c510de7f93bf1fa19e13423a606d8189a02624a800710f6e6a0a0f0784b3/librt-0.8.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:758509ea3f1eba2a57558e7e98f4659d0ea7670bff49673b0dde18a3c7e6c0eb", size = 198941, upload-time = "2026-02-17T16:11:44.28Z" }, - { url = "https://files.pythonhosted.org/packages/dd/36/e725903416409a533d92398e88ce665476f275081d0d7d42f9c4951999e5/librt-0.8.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:039b9f2c506bd0ab0f8725aa5ba339c6f0cd19d3b514b50d134789809c24285d", size = 209991, upload-time = "2026-02-17T16:11:45.462Z" }, - { url = "https://files.pythonhosted.org/packages/30/7a/8d908a152e1875c9f8eac96c97a480df425e657cdb47854b9efaa4998889/librt-0.8.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bb54f1205a3a6ab41a6fd71dfcdcbd278670d3a90ca502a30d9da583105b6f7", size = 224476, upload-time = "2026-02-17T16:11:46.542Z" }, - { url = "https://files.pythonhosted.org/packages/a8/b8/a22c34f2c485b8903a06f3fe3315341fe6876ef3599792344669db98fcff/librt-0.8.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:05bd41cdee35b0c59c259f870f6da532a2c5ca57db95b5f23689fcb5c9e42440", size = 217518, upload-time = "2026-02-17T16:11:47.746Z" }, - { url = "https://files.pythonhosted.org/packages/79/6f/5c6fea00357e4f82ba44f81dbfb027921f1ab10e320d4a64e1c408d035d9/librt-0.8.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adfab487facf03f0d0857b8710cf82d0704a309d8ffc33b03d9302b4c64e91a9", size = 225116, upload-time = "2026-02-17T16:11:49.298Z" }, - { url = "https://files.pythonhosted.org/packages/f2/a0/95ced4e7b1267fe1e2720a111685bcddf0e781f7e9e0ce59d751c44dcfe5/librt-0.8.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:153188fe98a72f206042be10a2c6026139852805215ed9539186312d50a8e972", size = 217751, upload-time = "2026-02-17T16:11:50.49Z" }, - { url = "https://files.pythonhosted.org/packages/93/c2/0517281cb4d4101c27ab59472924e67f55e375bc46bedae94ac6dc6e1902/librt-0.8.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:dd3c41254ee98604b08bd5b3af5bf0a89740d4ee0711de95b65166bf44091921", size = 218378, upload-time = "2026-02-17T16:11:51.783Z" }, - { url = "https://files.pythonhosted.org/packages/43/e8/37b3ac108e8976888e559a7b227d0ceac03c384cfd3e7a1c2ee248dbae79/librt-0.8.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e0d138c7ae532908cbb342162b2611dbd4d90c941cd25ab82084aaf71d2c0bd0", size = 241199, upload-time = "2026-02-17T16:11:53.561Z" }, - { url = "https://files.pythonhosted.org/packages/4b/5b/35812d041c53967fedf551a39399271bbe4257e681236a2cf1a69c8e7fa1/librt-0.8.1-cp312-cp312-win32.whl", hash = "sha256:43353b943613c5d9c49a25aaffdba46f888ec354e71e3529a00cca3f04d66a7a", size = 54917, upload-time = "2026-02-17T16:11:54.758Z" }, - { url = "https://files.pythonhosted.org/packages/de/d1/fa5d5331b862b9775aaf2a100f5ef86854e5d4407f71bddf102f4421e034/librt-0.8.1-cp312-cp312-win_amd64.whl", hash = "sha256:ff8baf1f8d3f4b6b7257fcb75a501f2a5499d0dda57645baa09d4d0d34b19444", size = 62017, upload-time = "2026-02-17T16:11:55.748Z" }, - { url = "https://files.pythonhosted.org/packages/c7/7c/c614252f9acda59b01a66e2ddfd243ed1c7e1deab0293332dfbccf862808/librt-0.8.1-cp312-cp312-win_arm64.whl", hash = "sha256:0f2ae3725904f7377e11cc37722d5d401e8b3d5851fb9273d7f4fe04f6b3d37d", size = 52441, upload-time = "2026-02-17T16:11:56.801Z" }, - { url = "https://files.pythonhosted.org/packages/c5/3c/f614c8e4eaac7cbf2bbdf9528790b21d89e277ee20d57dc6e559c626105f/librt-0.8.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7e6bad1cd94f6764e1e21950542f818a09316645337fd5ab9a7acc45d99a8f35", size = 66529, upload-time = "2026-02-17T16:11:57.809Z" }, - { url = "https://files.pythonhosted.org/packages/ab/96/5836544a45100ae411eda07d29e3d99448e5258b6e9c8059deb92945f5c2/librt-0.8.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cf450f498c30af55551ba4f66b9123b7185362ec8b625a773b3d39aa1a717583", size = 68669, upload-time = "2026-02-17T16:11:58.843Z" }, - { url = "https://files.pythonhosted.org/packages/06/53/f0b992b57af6d5531bf4677d75c44f095f2366a1741fb695ee462ae04b05/librt-0.8.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:eca45e982fa074090057132e30585a7e8674e9e885d402eae85633e9f449ce6c", size = 199279, upload-time = "2026-02-17T16:11:59.862Z" }, - { url = "https://files.pythonhosted.org/packages/f3/ad/4848cc16e268d14280d8168aee4f31cea92bbd2b79ce33d3e166f2b4e4fc/librt-0.8.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c3811485fccfda840861905b8c70bba5ec094e02825598bb9d4ca3936857a04", size = 210288, upload-time = "2026-02-17T16:12:00.954Z" }, - { url = "https://files.pythonhosted.org/packages/52/05/27fdc2e95de26273d83b96742d8d3b7345f2ea2bdbd2405cc504644f2096/librt-0.8.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e4af413908f77294605e28cfd98063f54b2c790561383971d2f52d113d9c363", size = 224809, upload-time = "2026-02-17T16:12:02.108Z" }, - { url = "https://files.pythonhosted.org/packages/7a/d0/78200a45ba3240cb042bc597d6f2accba9193a2c57d0356268cbbe2d0925/librt-0.8.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5212a5bd7fae98dae95710032902edcd2ec4dc994e883294f75c857b83f9aba0", size = 218075, upload-time = "2026-02-17T16:12:03.631Z" }, - { url = "https://files.pythonhosted.org/packages/af/72/a210839fa74c90474897124c064ffca07f8d4b347b6574d309686aae7ca6/librt-0.8.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e692aa2d1d604e6ca12d35e51fdc36f4cda6345e28e36374579f7ef3611b3012", size = 225486, upload-time = "2026-02-17T16:12:04.725Z" }, - { url = "https://files.pythonhosted.org/packages/a3/c1/a03cc63722339ddbf087485f253493e2b013039f5b707e8e6016141130fa/librt-0.8.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4be2a5c926b9770c9e08e717f05737a269b9d0ebc5d2f0060f0fe3fe9ce47acb", size = 218219, upload-time = "2026-02-17T16:12:05.828Z" }, - { url = "https://files.pythonhosted.org/packages/58/f5/fff6108af0acf941c6f274a946aea0e484bd10cd2dc37610287ce49388c5/librt-0.8.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fd1a720332ea335ceb544cf0a03f81df92abd4bb887679fd1e460976b0e6214b", size = 218750, upload-time = "2026-02-17T16:12:07.09Z" }, - { url = "https://files.pythonhosted.org/packages/71/67/5a387bfef30ec1e4b4f30562c8586566faf87e47d696768c19feb49e3646/librt-0.8.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2af9e01e0ef80d95ae3c720be101227edae5f2fe7e3dc63d8857fadfc5a1d", size = 241624, upload-time = "2026-02-17T16:12:08.43Z" }, - { url = "https://files.pythonhosted.org/packages/d4/be/24f8502db11d405232ac1162eb98069ca49c3306c1d75c6ccc61d9af8789/librt-0.8.1-cp313-cp313-win32.whl", hash = "sha256:086a32dbb71336627e78cc1d6ee305a68d038ef7d4c39aaff41ae8c9aa46e91a", size = 54969, upload-time = "2026-02-17T16:12:09.633Z" }, - { url = "https://files.pythonhosted.org/packages/5c/73/c9fdf6cb2a529c1a092ce769a12d88c8cca991194dfe641b6af12fa964d2/librt-0.8.1-cp313-cp313-win_amd64.whl", hash = "sha256:e11769a1dbda4da7b00a76cfffa67aa47cfa66921d2724539eee4b9ede780b79", size = 62000, upload-time = "2026-02-17T16:12:10.632Z" }, - { url = "https://files.pythonhosted.org/packages/d3/97/68f80ca3ac4924f250cdfa6e20142a803e5e50fca96ef5148c52ee8c10ea/librt-0.8.1-cp313-cp313-win_arm64.whl", hash = "sha256:924817ab3141aca17893386ee13261f1d100d1ef410d70afe4389f2359fea4f0", size = 52495, upload-time = "2026-02-17T16:12:11.633Z" }, - { url = "https://files.pythonhosted.org/packages/c9/6a/907ef6800f7bca71b525a05f1839b21f708c09043b1c6aa77b6b827b3996/librt-0.8.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6cfa7fe54fd4d1f47130017351a959fe5804bda7a0bc7e07a2cdbc3fdd28d34f", size = 66081, upload-time = "2026-02-17T16:12:12.766Z" }, - { url = "https://files.pythonhosted.org/packages/1b/18/25e991cd5640c9fb0f8d91b18797b29066b792f17bf8493da183bf5caabe/librt-0.8.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:228c2409c079f8c11fb2e5d7b277077f694cb93443eb760e00b3b83cb8b3176c", size = 68309, upload-time = "2026-02-17T16:12:13.756Z" }, - { url = "https://files.pythonhosted.org/packages/a4/36/46820d03f058cfb5a9de5940640ba03165ed8aded69e0733c417bb04df34/librt-0.8.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7aae78ab5e3206181780e56912d1b9bb9f90a7249ce12f0e8bf531d0462dd0fc", size = 196804, upload-time = "2026-02-17T16:12:14.818Z" }, - { url = "https://files.pythonhosted.org/packages/59/18/5dd0d3b87b8ff9c061849fbdb347758d1f724b9a82241aa908e0ec54ccd0/librt-0.8.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:172d57ec04346b047ca6af181e1ea4858086c80bdf455f61994c4aa6fc3f866c", size = 206907, upload-time = "2026-02-17T16:12:16.513Z" }, - { url = "https://files.pythonhosted.org/packages/d1/96/ef04902aad1424fd7299b62d1890e803e6ab4018c3044dca5922319c4b97/librt-0.8.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b1977c4ea97ce5eb7755a78fae68d87e4102e4aaf54985e8b56806849cc06a3", size = 221217, upload-time = "2026-02-17T16:12:17.906Z" }, - { url = "https://files.pythonhosted.org/packages/6d/ff/7e01f2dda84a8f5d280637a2e5827210a8acca9a567a54507ef1c75b342d/librt-0.8.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:10c42e1f6fd06733ef65ae7bebce2872bcafd8d6e6b0a08fe0a05a23b044fb14", size = 214622, upload-time = "2026-02-17T16:12:19.108Z" }, - { url = "https://files.pythonhosted.org/packages/1e/8c/5b093d08a13946034fed57619742f790faf77058558b14ca36a6e331161e/librt-0.8.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4c8dfa264b9193c4ee19113c985c95f876fae5e51f731494fc4e0cf594990ba7", size = 221987, upload-time = "2026-02-17T16:12:20.331Z" }, - { url = "https://files.pythonhosted.org/packages/d3/cc/86b0b3b151d40920ad45a94ce0171dec1aebba8a9d72bb3fa00c73ab25dd/librt-0.8.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:01170b6729a438f0dedc4a26ed342e3dc4f02d1000b4b19f980e1877f0c297e6", size = 215132, upload-time = "2026-02-17T16:12:21.54Z" }, - { url = "https://files.pythonhosted.org/packages/fc/be/8588164a46edf1e69858d952654e216a9a91174688eeefb9efbb38a9c799/librt-0.8.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:7b02679a0d783bdae30d443025b94465d8c3dc512f32f5b5031f93f57ac32071", size = 215195, upload-time = "2026-02-17T16:12:23.073Z" }, - { url = "https://files.pythonhosted.org/packages/f5/f2/0b9279bea735c734d69344ecfe056c1ba211694a72df10f568745c899c76/librt-0.8.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:190b109bb69592a3401fe1ffdea41a2e73370ace2ffdc4a0e8e2b39cdea81b78", size = 237946, upload-time = "2026-02-17T16:12:24.275Z" }, - { url = "https://files.pythonhosted.org/packages/e9/cc/5f2a34fbc8aeb35314a3641f9956fa9051a947424652fad9882be7a97949/librt-0.8.1-cp314-cp314-win32.whl", hash = "sha256:e70a57ecf89a0f64c24e37f38d3fe217a58169d2fe6ed6d70554964042474023", size = 50689, upload-time = "2026-02-17T16:12:25.766Z" }, - { url = "https://files.pythonhosted.org/packages/a0/76/cd4d010ab2147339ca2b93e959c3686e964edc6de66ddacc935c325883d7/librt-0.8.1-cp314-cp314-win_amd64.whl", hash = "sha256:7e2f3edca35664499fbb36e4770650c4bd4a08abc1f4458eab9df4ec56389730", size = 57875, upload-time = "2026-02-17T16:12:27.465Z" }, - { url = "https://files.pythonhosted.org/packages/84/0f/2143cb3c3ca48bd3379dcd11817163ca50781927c4537345d608b5045998/librt-0.8.1-cp314-cp314-win_arm64.whl", hash = "sha256:0d2f82168e55ddefd27c01c654ce52379c0750ddc31ee86b4b266bcf4d65f2a3", size = 48058, upload-time = "2026-02-17T16:12:28.556Z" }, - { url = "https://files.pythonhosted.org/packages/d2/0e/9b23a87e37baf00311c3efe6b48d6b6c168c29902dfc3f04c338372fd7db/librt-0.8.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2c74a2da57a094bd48d03fa5d196da83d2815678385d2978657499063709abe1", size = 68313, upload-time = "2026-02-17T16:12:29.659Z" }, - { url = "https://files.pythonhosted.org/packages/db/9a/859c41e5a4f1c84200a7d2b92f586aa27133c8243b6cac9926f6e54d01b9/librt-0.8.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a355d99c4c0d8e5b770313b8b247411ed40949ca44e33e46a4789b9293a907ee", size = 70994, upload-time = "2026-02-17T16:12:31.516Z" }, - { url = "https://files.pythonhosted.org/packages/4c/28/10605366ee599ed34223ac2bf66404c6fb59399f47108215d16d5ad751a8/librt-0.8.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2eb345e8b33fb748227409c9f1233d4df354d6e54091f0e8fc53acdb2ffedeb7", size = 220770, upload-time = "2026-02-17T16:12:33.294Z" }, - { url = "https://files.pythonhosted.org/packages/af/8d/16ed8fd452dafae9c48d17a6bc1ee3e818fd40ef718d149a8eff2c9f4ea2/librt-0.8.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9be2f15e53ce4e83cc08adc29b26fb5978db62ef2a366fbdf716c8a6c8901040", size = 235409, upload-time = "2026-02-17T16:12:35.443Z" }, - { url = "https://files.pythonhosted.org/packages/89/1b/7bdf3e49349c134b25db816e4a3db6b94a47ac69d7d46b1e682c2c4949be/librt-0.8.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:785ae29c1f5c6e7c2cde2c7c0e148147f4503da3abc5d44d482068da5322fd9e", size = 246473, upload-time = "2026-02-17T16:12:36.656Z" }, - { url = "https://files.pythonhosted.org/packages/4e/8a/91fab8e4fd2a24930a17188c7af5380eb27b203d72101c9cc000dbdfd95a/librt-0.8.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1d3a7da44baf692f0c6aeb5b2a09c5e6fc7a703bca9ffa337ddd2e2da53f7732", size = 238866, upload-time = "2026-02-17T16:12:37.849Z" }, - { url = "https://files.pythonhosted.org/packages/b9/e0/c45a098843fc7c07e18a7f8a24ca8496aecbf7bdcd54980c6ca1aaa79a8e/librt-0.8.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5fc48998000cbc39ec0d5311312dda93ecf92b39aaf184c5e817d5d440b29624", size = 250248, upload-time = "2026-02-17T16:12:39.445Z" }, - { url = "https://files.pythonhosted.org/packages/82/30/07627de23036640c952cce0c1fe78972e77d7d2f8fd54fa5ef4554ff4a56/librt-0.8.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e96baa6820280077a78244b2e06e416480ed859bbd8e5d641cf5742919d8beb4", size = 240629, upload-time = "2026-02-17T16:12:40.889Z" }, - { url = "https://files.pythonhosted.org/packages/fb/c1/55bfe1ee3542eba055616f9098eaf6eddb966efb0ca0f44eaa4aba327307/librt-0.8.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:31362dbfe297b23590530007062c32c6f6176f6099646bb2c95ab1b00a57c382", size = 239615, upload-time = "2026-02-17T16:12:42.446Z" }, - { url = "https://files.pythonhosted.org/packages/2b/39/191d3d28abc26c9099b19852e6c99f7f6d400b82fa5a4e80291bd3803e19/librt-0.8.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc3656283d11540ab0ea01978378e73e10002145117055e03722417aeab30994", size = 263001, upload-time = "2026-02-17T16:12:43.627Z" }, - { url = "https://files.pythonhosted.org/packages/b9/eb/7697f60fbe7042ab4e88f4ee6af496b7f222fffb0a4e3593ef1f29f81652/librt-0.8.1-cp314-cp314t-win32.whl", hash = "sha256:738f08021b3142c2918c03692608baed43bc51144c29e35807682f8070ee2a3a", size = 51328, upload-time = "2026-02-17T16:12:45.148Z" }, - { url = "https://files.pythonhosted.org/packages/7c/72/34bf2eb7a15414a23e5e70ecb9440c1d3179f393d9349338a91e2781c0fb/librt-0.8.1-cp314-cp314t-win_amd64.whl", hash = "sha256:89815a22daf9c51884fb5dbe4f1ef65ee6a146e0b6a8df05f753e2e4a9359bf4", size = 58722, upload-time = "2026-02-17T16:12:46.85Z" }, - { url = "https://files.pythonhosted.org/packages/b2/c8/d148e041732d631fc76036f8b30fae4e77b027a1e95b7a84bb522481a940/librt-0.8.1-cp314-cp314t-win_arm64.whl", hash = "sha256:bf512a71a23504ed08103a13c941f763db13fb11177beb3d9244c98c29fb4a61", size = 48755, upload-time = "2026-02-17T16:12:47.943Z" }, +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/eb/6b/3d5c13fb3e3c4f43206c8f9dfed13778c2ed4f000bacaa0b7ce3c402a265/librt-0.9.0.tar.gz", hash = "sha256:a0951822531e7aee6e0dfb556b30d5ee36bbe234faf60c20a16c01be3530869d", size = 184368, upload-time = "2026-04-09T16:06:26.173Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/1e/2ec7afcebcf3efea593d13aee18bbcfdd3a243043d848ebf385055e9f636/librt-0.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:90904fac73c478f4b83f4ed96c99c8208b75e6f9a8a1910548f69a00f1eaa671", size = 67155, upload-time = "2026-04-09T16:04:42.933Z" }, + { url = "https://files.pythonhosted.org/packages/18/77/72b85afd4435268338ad4ec6231b3da8c77363f212a0227c1ff3b45e4d35/librt-0.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:789fff71757facc0738e8d89e3b84e4f0251c1c975e85e81b152cdaca927cc2d", size = 69916, upload-time = "2026-04-09T16:04:44.042Z" }, + { url = "https://files.pythonhosted.org/packages/27/fb/948ea0204fbe2e78add6d46b48330e58d39897e425560674aee302dca81c/librt-0.9.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1bf465d1e5b0a27713862441f6467b5ab76385f4ecf8f1f3a44f8aa3c695b4b6", size = 199635, upload-time = "2026-04-09T16:04:45.5Z" }, + { url = "https://files.pythonhosted.org/packages/ac/cd/894a29e251b296a27957856804cfd21e93c194aa131de8bb8032021be07e/librt-0.9.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f819e0c6413e259a17a7c0d49f97f405abadd3c2a316a3b46c6440b7dbbedbb1", size = 211051, upload-time = "2026-04-09T16:04:47.016Z" }, + { url = "https://files.pythonhosted.org/packages/18/8f/dcaed0bc084a35f3721ff2d081158db569d2c57ea07d35623ddaca5cfc8e/librt-0.9.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e0785c2fb4a81e1aece366aa3e2e039f4a4d7d21aaaded5227d7f3c703427882", size = 224031, upload-time = "2026-04-09T16:04:48.207Z" }, + { url = "https://files.pythonhosted.org/packages/03/44/88f6c1ed1132cd418601cc041fbd92fed28b3a09f39de81978e0822d13ff/librt-0.9.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:80b25c7b570a86c03b5da69e665809deb39265476e8e21d96a9328f9762f9990", size = 218069, upload-time = "2026-04-09T16:04:50.025Z" }, + { url = "https://files.pythonhosted.org/packages/a3/90/7d02e981c2db12188d82b4410ff3e35bfdb844b26aecd02233626f46af2b/librt-0.9.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d4d16b608a1c43d7e33142099a75cd93af482dadce0bf82421e91cad077157f4", size = 224857, upload-time = "2026-04-09T16:04:51.684Z" }, + { url = "https://files.pythonhosted.org/packages/ef/c3/c77e706b7215ca32e928d47535cf13dbc3d25f096f84ddf8fbc06693e229/librt-0.9.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:194fc1a32e1e21fe809d38b5faea66cc65eaa00217c8901fbdb99866938adbdb", size = 219865, upload-time = "2026-04-09T16:04:52.949Z" }, + { url = "https://files.pythonhosted.org/packages/52/d1/32b0c1a0eb8461c70c11656c46a29f760b7c7edf3c36d6f102470c17170f/librt-0.9.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:8c6bc1384d9738781cfd41d09ad7f6e8af13cfea2c75ece6bd6d2566cdea2076", size = 218451, upload-time = "2026-04-09T16:04:54.174Z" }, + { url = "https://files.pythonhosted.org/packages/74/d1/adfd0f9c44761b1d49b1bec66173389834c33ee2bd3c7fd2e2367f1942d4/librt-0.9.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:15cb151e52a044f06e54ac7f7b47adbfc89b5c8e2b63e1175a9d587c43e8942a", size = 241300, upload-time = "2026-04-09T16:04:55.452Z" }, + { url = "https://files.pythonhosted.org/packages/09/b0/9074b64407712f0003c27f5b1d7655d1438979155f049720e8a1abd9b1a1/librt-0.9.0-cp311-cp311-win32.whl", hash = "sha256:f100bfe2acf8a3689af9d0cc660d89f17286c9c795f9f18f7b62dd1a6b247ae6", size = 55668, upload-time = "2026-04-09T16:04:56.689Z" }, + { url = "https://files.pythonhosted.org/packages/24/19/40b77b77ce80b9389fb03971431b09b6b913911c38d412059e0b3e2a9ef2/librt-0.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:0b73e4266307e51c95e09c0750b7ec383c561d2e97d58e473f6f6a209952fbb8", size = 62976, upload-time = "2026-04-09T16:04:57.733Z" }, + { url = "https://files.pythonhosted.org/packages/70/9d/9fa7a64041e29035cb8c575af5f0e3840be1b97b4c4d9061e0713f171849/librt-0.9.0-cp311-cp311-win_arm64.whl", hash = "sha256:bc5518873822d2faa8ebdd2c1a4d7c8ef47b01a058495ab7924cb65bdbf5fc9a", size = 53502, upload-time = "2026-04-09T16:04:58.806Z" }, + { url = "https://files.pythonhosted.org/packages/bf/90/89ddba8e1c20b0922783cd93ed8e64f34dc05ab59c38a9c7e313632e20ff/librt-0.9.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9b3e3bc363f71bda1639a4ee593cb78f7fbfeacc73411ec0d4c92f00730010a4", size = 68332, upload-time = "2026-04-09T16:05:00.09Z" }, + { url = "https://files.pythonhosted.org/packages/a8/40/7aa4da1fb08bdeeb540cb07bfc8207cb32c5c41642f2594dbd0098a0662d/librt-0.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0a09c2f5869649101738653a9b7ab70cf045a1105ac66cbb8f4055e61df78f2d", size = 70581, upload-time = "2026-04-09T16:05:01.213Z" }, + { url = "https://files.pythonhosted.org/packages/48/ac/73a2187e1031041e93b7e3a25aae37aa6f13b838c550f7e0f06f66766212/librt-0.9.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5ca8e133d799c948db2ab1afc081c333a825b5540475164726dcbf73537e5c2f", size = 203984, upload-time = "2026-04-09T16:05:02.542Z" }, + { url = "https://files.pythonhosted.org/packages/5e/3d/23460d571e9cbddb405b017681df04c142fb1b04cbfce77c54b08e28b108/librt-0.9.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:603138ee838ee1583f1b960b62d5d0007845c5c423feb68e44648b1359014e27", size = 215762, upload-time = "2026-04-09T16:05:04.127Z" }, + { url = "https://files.pythonhosted.org/packages/de/1e/42dc7f8ab63e65b20640d058e63e97fd3e482c1edbda3570d813b4d0b927/librt-0.9.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f4003f70c56a5addd6aa0897f200dd59afd3bf7bcd5b3cce46dd21f925743bc2", size = 230288, upload-time = "2026-04-09T16:05:05.883Z" }, + { url = "https://files.pythonhosted.org/packages/dc/08/ca812b6d8259ad9ece703397f8ad5c03af5b5fedfce64279693d3ce4087c/librt-0.9.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:78042f6facfd98ecb25e9829c7e37cce23363d9d7c83bc5f72702c5059eb082b", size = 224103, upload-time = "2026-04-09T16:05:07.148Z" }, + { url = "https://files.pythonhosted.org/packages/b6/3f/620490fb2fa66ffd44e7f900254bc110ebec8dac6c1b7514d64662570e6f/librt-0.9.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a361c9434a64d70a7dbb771d1de302c0cc9f13c0bffe1cf7e642152814b35265", size = 232122, upload-time = "2026-04-09T16:05:08.386Z" }, + { url = "https://files.pythonhosted.org/packages/e9/83/12864700a1b6a8be458cf5d05db209b0d8e94ae281e7ec261dbe616597b4/librt-0.9.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:dd2c7e082b0b92e1baa4da28163a808672485617bc855cc22a2fd06978fa9084", size = 225045, upload-time = "2026-04-09T16:05:09.707Z" }, + { url = "https://files.pythonhosted.org/packages/fd/1b/845d339c29dc7dbc87a2e992a1ba8d28d25d0e0372f9a0a2ecebde298186/librt-0.9.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7e6274fd33fc5b2a14d41c9119629d3ff395849d8bcbc80cf637d9e8d2034da8", size = 227372, upload-time = "2026-04-09T16:05:10.942Z" }, + { url = "https://files.pythonhosted.org/packages/8d/fe/277985610269d926a64c606f761d58d3db67b956dbbf40024921e95e7fcb/librt-0.9.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5093043afb226ecfa1400120d1ebd4442b4f99977783e4f4f7248879009b227f", size = 248224, upload-time = "2026-04-09T16:05:12.254Z" }, + { url = "https://files.pythonhosted.org/packages/92/1b/ee486d244b8de6b8b5dbaefabe6bfdd4a72e08f6353edf7d16d27114da8d/librt-0.9.0-cp312-cp312-win32.whl", hash = "sha256:9edcc35d1cae9fd5320171b1a838c7da8a5c968af31e82ecc3dff30b4be0957f", size = 55986, upload-time = "2026-04-09T16:05:13.529Z" }, + { url = "https://files.pythonhosted.org/packages/89/7a/ba1737012308c17dc6d5516143b5dce9a2c7ba3474afd54e11f44a4d1ef3/librt-0.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:3cc2917258e131ae5f958a4d872e07555b51cb7466a43433218061c74ef33745", size = 63260, upload-time = "2026-04-09T16:05:14.68Z" }, + { url = "https://files.pythonhosted.org/packages/36/e4/01752c113da15127f18f7bf11142f5640038f062407a611c059d0036c6aa/librt-0.9.0-cp312-cp312-win_arm64.whl", hash = "sha256:90e6d5420fc8a300518d4d2288154ff45005e920425c22cbbfe8330f3f754bd9", size = 53694, upload-time = "2026-04-09T16:05:16.095Z" }, + { url = "https://files.pythonhosted.org/packages/5f/d7/1b3e26fffde1452d82f5666164858a81c26ebe808e7ae8c9c88628981540/librt-0.9.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f29b68cd9714531672db62cc54f6e8ff981900f824d13fa0e00749189e13778e", size = 68367, upload-time = "2026-04-09T16:05:17.243Z" }, + { url = "https://files.pythonhosted.org/packages/a5/5b/c61b043ad2e091fbe1f2d35d14795e545d0b56b03edaa390fa1dcee3d160/librt-0.9.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7d5c8a5929ac325729f6119802070b561f4db793dffc45e9ac750992a4ed4d22", size = 70595, upload-time = "2026-04-09T16:05:18.471Z" }, + { url = "https://files.pythonhosted.org/packages/a3/22/2448471196d8a73370aa2f23445455dc42712c21404081fcd7a03b9e0749/librt-0.9.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:756775d25ec8345b837ab52effee3ad2f3b2dfd6bbee3e3f029c517bd5d8f05a", size = 204354, upload-time = "2026-04-09T16:05:19.593Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5e/39fc4b153c78cfd2c8a2dcb32700f2d41d2312aa1050513183be4540930d/librt-0.9.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b8f5d00b49818f4e2b1667db994488b045835e0ac16fe2f924f3871bd2b8ac5", size = 216238, upload-time = "2026-04-09T16:05:20.868Z" }, + { url = "https://files.pythonhosted.org/packages/d7/42/bc2d02d0fa7badfa63aa8d6dcd8793a9f7ef5a94396801684a51ed8d8287/librt-0.9.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c81aef782380f0f13ead670aae01825eb653b44b046aa0e5ebbb79f76ed4aa11", size = 230589, upload-time = "2026-04-09T16:05:22.305Z" }, + { url = "https://files.pythonhosted.org/packages/c8/7b/e2d95cc513866373692aa5edf98080d5602dd07cabfb9e5d2f70df2f25f7/librt-0.9.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:66b58fed90a545328e80d575467244de3741e088c1af928f0b489ebec3ef3858", size = 224610, upload-time = "2026-04-09T16:05:23.647Z" }, + { url = "https://files.pythonhosted.org/packages/31/d5/6cec4607e998eaba57564d06a1295c21b0a0c8de76e4e74d699e627bd98c/librt-0.9.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e78fb7419e07d98c2af4b8567b72b3eaf8cb05caad642e9963465569c8b2d87e", size = 232558, upload-time = "2026-04-09T16:05:25.025Z" }, + { url = "https://files.pythonhosted.org/packages/95/8c/27f1d8d3aaf079d3eb26439bf0b32f1482340c3552e324f7db9dca858671/librt-0.9.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c3786f0f4490a5cd87f1ed6cefae833ad6b1060d52044ce0434a2e85893afd0", size = 225521, upload-time = "2026-04-09T16:05:26.311Z" }, + { url = "https://files.pythonhosted.org/packages/6b/d8/1e0d43b1c329b416017619469b3c3801a25a6a4ef4a1c68332aeaa6f72ca/librt-0.9.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:8494cfc61e03542f2d381e71804990b3931175a29b9278fdb4a5459948778dc2", size = 227789, upload-time = "2026-04-09T16:05:27.624Z" }, + { url = "https://files.pythonhosted.org/packages/2c/b4/d3d842e88610fcd4c8eec7067b0c23ef2d7d3bff31496eded6a83b0f99be/librt-0.9.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:07cf11f769831186eeac424376e6189f20ace4f7263e2134bdb9757340d84d4d", size = 248616, upload-time = "2026-04-09T16:05:29.181Z" }, + { url = "https://files.pythonhosted.org/packages/ec/28/527df8ad0d1eb6c8bdfa82fc190f1f7c4cca5a1b6d7b36aeabf95b52d74d/librt-0.9.0-cp313-cp313-win32.whl", hash = "sha256:850d6d03177e52700af605fd60db7f37dcb89782049a149674d1a9649c2138fd", size = 56039, upload-time = "2026-04-09T16:05:30.709Z" }, + { url = "https://files.pythonhosted.org/packages/f3/a7/413652ad0d92273ee5e30c000fc494b361171177c83e57c060ecd3c21538/librt-0.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:a5af136bfba820d592f86c67affcef9b3ff4d4360ac3255e341e964489b48519", size = 63264, upload-time = "2026-04-09T16:05:31.881Z" }, + { url = "https://files.pythonhosted.org/packages/a4/0a/92c244309b774e290ddb15e93363846ae7aa753d9586b8aad511c5e6145b/librt-0.9.0-cp313-cp313-win_arm64.whl", hash = "sha256:4c4d0440a3a8e31d962340c3e1cc3fc9ee7febd34c8d8f770d06adb947779ea5", size = 53728, upload-time = "2026-04-09T16:05:33.31Z" }, + { url = "https://files.pythonhosted.org/packages/cd/c1/184e539543f06ea2912f4b92a5ffaede4f9b392689e3f00acbf8134bee92/librt-0.9.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:3f05d145df35dca5056a8bc3838e940efebd893a54b3e19b2dda39ceaa299bcb", size = 67830, upload-time = "2026-04-09T16:05:34.517Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ad/23399bdcb7afca819acacdef31b37ee59de261bd66b503a7995c03c4b0dc/librt-0.9.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1c587494461ebd42229d0f1739f3aa34237dd9980623ecf1be8d3bcba79f4499", size = 70280, upload-time = "2026-04-09T16:05:35.649Z" }, + { url = "https://files.pythonhosted.org/packages/9f/0b/4542dc5a2b8772dbf92cafb9194701230157e73c14b017b6961a23598b03/librt-0.9.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b0a2040f801406b93657a70b72fa12311063a319fee72ce98e1524da7200171f", size = 201925, upload-time = "2026-04-09T16:05:36.739Z" }, + { url = "https://files.pythonhosted.org/packages/31/d4/8ee7358b08fd0cfce051ef96695380f09b3c2c11b77c9bfbc367c921cce5/librt-0.9.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f38bc489037eca88d6ebefc9c4d41a4e07c8e8b4de5188a9e6d290273ad7ebb1", size = 212381, upload-time = "2026-04-09T16:05:38.043Z" }, + { url = "https://files.pythonhosted.org/packages/f2/94/a2025fe442abedf8b038038dab3dba942009ad42b38ea064a1a9e6094241/librt-0.9.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3fd278f5e6bf7c75ccd6d12344eb686cc020712683363b66f46ac79d37c799f", size = 227065, upload-time = "2026-04-09T16:05:39.394Z" }, + { url = "https://files.pythonhosted.org/packages/7c/e9/b9fcf6afa909f957cfbbf918802f9dada1bd5d3c1da43d722fd6a310dc3f/librt-0.9.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fcbdf2a9ca24e87bbebb47f1fe34e531ef06f104f98c9ccfc953a3f3344c567a", size = 221333, upload-time = "2026-04-09T16:05:40.999Z" }, + { url = "https://files.pythonhosted.org/packages/ac/7c/ba54cd6aa6a3c8cd12757a6870e0c79a64b1e6327f5248dcff98423f4d43/librt-0.9.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e306d956cfa027fe041585f02a1602c32bfa6bb8ebea4899d373383295a6c62f", size = 229051, upload-time = "2026-04-09T16:05:42.605Z" }, + { url = "https://files.pythonhosted.org/packages/4b/4b/8cfdbad314c8677a0148bf0b70591d6d18587f9884d930276098a235461b/librt-0.9.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:465814ab157986acb9dfa5ccd7df944be5eefc0d08d31ec6e8d88bc71251d845", size = 222492, upload-time = "2026-04-09T16:05:43.842Z" }, + { url = "https://files.pythonhosted.org/packages/1f/d1/2eda69563a1a88706808decdce035e4b32755dbfbb0d05e1a65db9547ed1/librt-0.9.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:703f4ae36d6240bfe24f542bac784c7e4194ec49c3ba5a994d02891649e2d85b", size = 223849, upload-time = "2026-04-09T16:05:45.054Z" }, + { url = "https://files.pythonhosted.org/packages/04/44/b2ed37df6be5b3d42cfe36318e0598e80843d5c6308dd63d0bf4e0ce5028/librt-0.9.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3be322a15ee5e70b93b7a59cfd074614f22cc8c9ff18bd27f474e79137ea8d3b", size = 245001, upload-time = "2026-04-09T16:05:46.34Z" }, + { url = "https://files.pythonhosted.org/packages/47/e7/617e412426df89169dd2a9ed0cc8752d5763336252c65dbf945199915119/librt-0.9.0-cp314-cp314-win32.whl", hash = "sha256:b8da9f8035bb417770b1e1610526d87ad4fc58a2804dc4d79c53f6d2cf5a6eb9", size = 51799, upload-time = "2026-04-09T16:05:47.738Z" }, + { url = "https://files.pythonhosted.org/packages/24/ed/c22ca4db0ca3cbc285e4d9206108746beda561a9792289c3c31281d7e9df/librt-0.9.0-cp314-cp314-win_amd64.whl", hash = "sha256:b8bd70d5d816566a580d193326912f4a76ec2d28a97dc4cd4cc831c0af8e330e", size = 59165, upload-time = "2026-04-09T16:05:49.198Z" }, + { url = "https://files.pythonhosted.org/packages/24/56/875398fafa4cbc8f15b89366fc3287304ddd3314d861f182a4b87595ace0/librt-0.9.0-cp314-cp314-win_arm64.whl", hash = "sha256:fc5758e2b7a56532dc33e3c544d78cbaa9ecf0a0f2a2da2df882c1d6b99a317f", size = 49292, upload-time = "2026-04-09T16:05:50.362Z" }, + { url = "https://files.pythonhosted.org/packages/4c/61/bc448ecbf9b2d69c5cff88fe41496b19ab2a1cbda0065e47d4d0d51c0867/librt-0.9.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:f24b90b0e0c8cc9491fb1693ae91fe17cb7963153a1946395acdbdd5818429a4", size = 70175, upload-time = "2026-04-09T16:05:51.564Z" }, + { url = "https://files.pythonhosted.org/packages/60/f2/c47bb71069a73e2f04e70acbd196c1e5cc411578ac99039a224b98920fd4/librt-0.9.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3fe56e80badb66fdcde06bef81bbaa5bfcf6fbd7aefb86222d9e369c38c6b228", size = 72951, upload-time = "2026-04-09T16:05:52.699Z" }, + { url = "https://files.pythonhosted.org/packages/29/19/0549df59060631732df758e8886d92088da5fdbedb35b80e4643664e8412/librt-0.9.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:527b5b820b47a09e09829051452bb0d1dd2122261254e2a6f674d12f1d793d54", size = 225864, upload-time = "2026-04-09T16:05:53.895Z" }, + { url = "https://files.pythonhosted.org/packages/9d/f8/3b144396d302ac08e50f89e64452c38db84bc7b23f6c60479c5d3abd303c/librt-0.9.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d429bdd4ac0ab17c8e4a8af0ed2a7440b16eba474909ab357131018fe8c7e71", size = 241155, upload-time = "2026-04-09T16:05:55.191Z" }, + { url = "https://files.pythonhosted.org/packages/7a/ce/ee67ec14581de4043e61d05786d2aed6c9b5338816b7859bcf07455c6a9f/librt-0.9.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7202bdcac47d3a708271c4304a474a8605a4a9a4a709e954bf2d3241140aa938", size = 252235, upload-time = "2026-04-09T16:05:56.549Z" }, + { url = "https://files.pythonhosted.org/packages/8a/fa/0ead15daa2b293a54101550b08d4bafe387b7d4a9fc6d2b985602bae69b6/librt-0.9.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0d620e74897f8c2613b3c4e2e9c1e422eb46d2ddd07df540784d44117836af3", size = 244963, upload-time = "2026-04-09T16:05:57.858Z" }, + { url = "https://files.pythonhosted.org/packages/29/68/9fbf9a9aa704ba87689e40017e720aced8d9a4d2b46b82451d8142f91ec9/librt-0.9.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d69fc39e627908f4c03297d5a88d9284b73f4d90b424461e32e8c2485e21c283", size = 257364, upload-time = "2026-04-09T16:05:59.686Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8d/9d60869f1b6716c762e45f66ed945b1e5dd649f7377684c3b176ae424648/librt-0.9.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:c2640e23d2b7c98796f123ffd95cf2022c7777aa8a4a3b98b36c570d37e85eee", size = 247661, upload-time = "2026-04-09T16:06:00.938Z" }, + { url = "https://files.pythonhosted.org/packages/70/ff/a5c365093962310bfdb4f6af256f191085078ffb529b3f0cbebb5b33ebe2/librt-0.9.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:451daa98463b7695b0a30aa56bf637831ea559e7b8101ac2ef6382e8eb15e29c", size = 248238, upload-time = "2026-04-09T16:06:02.537Z" }, + { url = "https://files.pythonhosted.org/packages/a0/3c/2d34365177f412c9e19c0a29f969d70f5343f27634b76b765a54d8b27705/librt-0.9.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:928bd06eca2c2bbf4349e5b817f837509b0604342e65a502de1d50a7570afd15", size = 269457, upload-time = "2026-04-09T16:06:03.833Z" }, + { url = "https://files.pythonhosted.org/packages/bc/cd/de45b239ea3bdf626f982a00c14bfcf2e12d261c510ba7db62c5969a27cd/librt-0.9.0-cp314-cp314t-win32.whl", hash = "sha256:a9c63e04d003bc0fb6a03b348018b9a3002f98268200e22cc80f146beac5dc40", size = 52453, upload-time = "2026-04-09T16:06:05.229Z" }, + { url = "https://files.pythonhosted.org/packages/7f/f9/bfb32ae428aa75c0c533915622176f0a17d6da7b72b5a3c6363685914f70/librt-0.9.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f162af66a2ed3f7d1d161a82ca584efd15acd9c1cff190a373458c32f7d42118", size = 60044, upload-time = "2026-04-09T16:06:06.398Z" }, + { url = "https://files.pythonhosted.org/packages/aa/47/7d70414bcdbb3bc1f458a8d10558f00bbfdb24e5a11740fc8197e12c3255/librt-0.9.0-cp314-cp314t-win_arm64.whl", hash = "sha256:a4b25c6c25cac5d0d9d6d6da855195b254e0021e513e0249f0e3b444dc6e0e61", size = 50009, upload-time = "2026-04-09T16:06:07.995Z" }, ] [[package]] @@ -1257,7 +1312,7 @@ wheels = [ [[package]] name = "mypy" -version = "1.19.1" +version = "1.20.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, @@ -1265,33 +1320,44 @@ dependencies = [ { name = "pathspec" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f5/db/4efed9504bc01309ab9c2da7e352cc223569f05478012b5d9ece38fd44d2/mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba", size = 3582404, upload-time = "2025-12-15T05:03:48.42Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/47/6b3ebabd5474d9cdc170d1342fbf9dddc1b0ec13ec90bf9004ee6f391c31/mypy-1.19.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d8dfc6ab58ca7dda47d9237349157500468e404b17213d44fc1cb77bce532288", size = 13028539, upload-time = "2025-12-15T05:03:44.129Z" }, - { url = "https://files.pythonhosted.org/packages/5c/a6/ac7c7a88a3c9c54334f53a941b765e6ec6c4ebd65d3fe8cdcfbe0d0fd7db/mypy-1.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e3f276d8493c3c97930e354b2595a44a21348b320d859fb4a2b9f66da9ed27ab", size = 12083163, upload-time = "2025-12-15T05:03:37.679Z" }, - { url = "https://files.pythonhosted.org/packages/67/af/3afa9cf880aa4a2c803798ac24f1d11ef72a0c8079689fac5cfd815e2830/mypy-1.19.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2abb24cf3f17864770d18d673c85235ba52456b36a06b6afc1e07c1fdcd3d0e6", size = 12687629, upload-time = "2025-12-15T05:02:31.526Z" }, - { url = "https://files.pythonhosted.org/packages/2d/46/20f8a7114a56484ab268b0ab372461cb3a8f7deed31ea96b83a4e4cfcfca/mypy-1.19.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a009ffa5a621762d0c926a078c2d639104becab69e79538a494bcccb62cc0331", size = 13436933, upload-time = "2025-12-15T05:03:15.606Z" }, - { url = "https://files.pythonhosted.org/packages/5b/f8/33b291ea85050a21f15da910002460f1f445f8007adb29230f0adea279cb/mypy-1.19.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f7cee03c9a2e2ee26ec07479f38ea9c884e301d42c6d43a19d20fb014e3ba925", size = 13661754, upload-time = "2025-12-15T05:02:26.731Z" }, - { url = "https://files.pythonhosted.org/packages/fd/a3/47cbd4e85bec4335a9cd80cf67dbc02be21b5d4c9c23ad6b95d6c5196bac/mypy-1.19.1-cp311-cp311-win_amd64.whl", hash = "sha256:4b84a7a18f41e167f7995200a1d07a4a6810e89d29859df936f1c3923d263042", size = 10055772, upload-time = "2025-12-15T05:03:26.179Z" }, - { url = "https://files.pythonhosted.org/packages/06/8a/19bfae96f6615aa8a0604915512e0289b1fad33d5909bf7244f02935d33a/mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1", size = 13206053, upload-time = "2025-12-15T05:03:46.622Z" }, - { url = "https://files.pythonhosted.org/packages/a5/34/3e63879ab041602154ba2a9f99817bb0c85c4df19a23a1443c8986e4d565/mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e", size = 12219134, upload-time = "2025-12-15T05:03:24.367Z" }, - { url = "https://files.pythonhosted.org/packages/89/cc/2db6f0e95366b630364e09845672dbee0cbf0bbe753a204b29a944967cd9/mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2", size = 12731616, upload-time = "2025-12-15T05:02:44.725Z" }, - { url = "https://files.pythonhosted.org/packages/00/be/dd56c1fd4807bc1eba1cf18b2a850d0de7bacb55e158755eb79f77c41f8e/mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8", size = 13620847, upload-time = "2025-12-15T05:03:39.633Z" }, - { url = "https://files.pythonhosted.org/packages/6d/42/332951aae42b79329f743bf1da088cd75d8d4d9acc18fbcbd84f26c1af4e/mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a", size = 13834976, upload-time = "2025-12-15T05:03:08.786Z" }, - { url = "https://files.pythonhosted.org/packages/6f/63/e7493e5f90e1e085c562bb06e2eb32cae27c5057b9653348d38b47daaecc/mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13", size = 10118104, upload-time = "2025-12-15T05:03:10.834Z" }, - { url = "https://files.pythonhosted.org/packages/de/9f/a6abae693f7a0c697dbb435aac52e958dc8da44e92e08ba88d2e42326176/mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250", size = 13201927, upload-time = "2025-12-15T05:02:29.138Z" }, - { url = "https://files.pythonhosted.org/packages/9a/a4/45c35ccf6e1c65afc23a069f50e2c66f46bd3798cbe0d680c12d12935caa/mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b", size = 12206730, upload-time = "2025-12-15T05:03:01.325Z" }, - { url = "https://files.pythonhosted.org/packages/05/bb/cdcf89678e26b187650512620eec8368fded4cfd99cfcb431e4cdfd19dec/mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e", size = 12724581, upload-time = "2025-12-15T05:03:20.087Z" }, - { url = "https://files.pythonhosted.org/packages/d1/32/dd260d52babf67bad8e6770f8e1102021877ce0edea106e72df5626bb0ec/mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef", size = 13616252, upload-time = "2025-12-15T05:02:49.036Z" }, - { url = "https://files.pythonhosted.org/packages/71/d0/5e60a9d2e3bd48432ae2b454b7ef2b62a960ab51292b1eda2a95edd78198/mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75", size = 13840848, upload-time = "2025-12-15T05:02:55.95Z" }, - { url = "https://files.pythonhosted.org/packages/98/76/d32051fa65ecf6cc8c6610956473abdc9b4c43301107476ac03559507843/mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd", size = 10135510, upload-time = "2025-12-15T05:02:58.438Z" }, - { url = "https://files.pythonhosted.org/packages/de/eb/b83e75f4c820c4247a58580ef86fcd35165028f191e7e1ba57128c52782d/mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1", size = 13199744, upload-time = "2025-12-15T05:03:30.823Z" }, - { url = "https://files.pythonhosted.org/packages/94/28/52785ab7bfa165f87fcbb61547a93f98bb20e7f82f90f165a1f69bce7b3d/mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718", size = 12215815, upload-time = "2025-12-15T05:02:42.323Z" }, - { url = "https://files.pythonhosted.org/packages/0a/c6/bdd60774a0dbfb05122e3e925f2e9e846c009e479dcec4821dad881f5b52/mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b", size = 12740047, upload-time = "2025-12-15T05:03:33.168Z" }, - { url = "https://files.pythonhosted.org/packages/32/2a/66ba933fe6c76bd40d1fe916a83f04fed253152f451a877520b3c4a5e41e/mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045", size = 13601998, upload-time = "2025-12-15T05:03:13.056Z" }, - { url = "https://files.pythonhosted.org/packages/e3/da/5055c63e377c5c2418760411fd6a63ee2b96cf95397259038756c042574f/mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957", size = 13807476, upload-time = "2025-12-15T05:03:17.977Z" }, - { url = "https://files.pythonhosted.org/packages/cd/09/4ebd873390a063176f06b0dbf1f7783dd87bd120eae7727fa4ae4179b685/mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f", size = 10281872, upload-time = "2025-12-15T05:03:05.549Z" }, - { url = "https://files.pythonhosted.org/packages/8d/f4/4ce9a05ce5ded1de3ec1c1d96cf9f9504a04e54ce0ed55cfa38619a32b8d/mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247", size = 2471239, upload-time = "2025-12-15T05:03:07.248Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/0b/3d/5b373635b3146264eb7a68d09e5ca11c305bbb058dfffbb47c47daf4f632/mypy-1.20.1.tar.gz", hash = "sha256:6fc3f4ecd52de81648fed1945498bf42fa2993ddfad67c9056df36ae5757f804", size = 3815892, upload-time = "2026-04-13T02:46:51.474Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/0d/555ab7453cc4a4a8643b7f21c842b1a84c36b15392061ae7b052ee119320/mypy-1.20.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c01eb9bac2c6a962d00f9d23421cd2913840e65bba365167d057bd0b4171a92e", size = 14336012, upload-time = "2026-04-13T02:45:39.935Z" }, + { url = "https://files.pythonhosted.org/packages/57/26/85a28893f7db8a16ebb41d1e9dfcb4475844d06a88480b6639e32a74d6ef/mypy-1.20.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:55d12ddbd8a9cac5b276878bd534fa39fff5bf543dc6ae18f25d30c8d7d27fca", size = 13224636, upload-time = "2026-04-13T02:45:49.659Z" }, + { url = "https://files.pythonhosted.org/packages/93/41/bd4cd3c2caeb6c448b669222b8cfcbdee4a03b89431527b56fca9e56b6f3/mypy-1.20.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0aa322c1468b6cdfc927a44ce130f79bb44bcd34eb4a009eb9f96571fd80955", size = 13663471, upload-time = "2026-04-13T02:46:20.276Z" }, + { url = "https://files.pythonhosted.org/packages/3e/56/7ee8c471e10402d64b6517ae10434541baca053cffd81090e4097d5609d4/mypy-1.20.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3f8bc95899cf676b6e2285779a08a998cc3a7b26f1026752df9d2741df3c79e8", size = 14532344, upload-time = "2026-04-13T02:46:44.205Z" }, + { url = "https://files.pythonhosted.org/packages/b5/95/b37d1fa859a433f6156742e12f62b0bb75af658544fb6dada9363918743a/mypy-1.20.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:47c2b90191a870a04041e910277494b0d92f0711be9e524d45c074fe60c00b65", size = 14776670, upload-time = "2026-04-13T02:45:52.481Z" }, + { url = "https://files.pythonhosted.org/packages/03/77/b302e4cb0b80d2bdf6bf4fce5864bb4cbfa461f7099cea544eaf2457df78/mypy-1.20.1-cp311-cp311-win_amd64.whl", hash = "sha256:9857dc8d2ec1a392ffbda518075beb00ac58859979c79f9e6bdcb7277082c2f2", size = 10816524, upload-time = "2026-04-13T02:45:37.711Z" }, + { url = "https://files.pythonhosted.org/packages/7f/21/d969d7a68eb964993ebcc6170d5ecaf0cf65830c58ac3344562e16dc42a9/mypy-1.20.1-cp311-cp311-win_arm64.whl", hash = "sha256:09d8df92bb25b6065ab91b178da843dda67b33eb819321679a6e98a907ce0e10", size = 9750419, upload-time = "2026-04-13T02:45:08.542Z" }, + { url = "https://files.pythonhosted.org/packages/69/1b/75a7c825a02781ca10bc2f2f12fba2af5202f6d6005aad8d2d1f264d8d78/mypy-1.20.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:36ee2b9c6599c230fea89bbd79f401f9f9f8e9fcf0c777827789b19b7da90f51", size = 14494077, upload-time = "2026-04-13T02:45:55.085Z" }, + { url = "https://files.pythonhosted.org/packages/b0/54/5e5a569ea5c2b4d48b729fb32aa936eeb4246e4fc3e6f5b3d36a2dfbefb9/mypy-1.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fba3fb0968a7b48806b0c90f38d39296f10766885a94c83bd21399de1e14eb28", size = 13319495, upload-time = "2026-04-13T02:45:29.674Z" }, + { url = "https://files.pythonhosted.org/packages/6f/a4/a1945b19f33e91721b59deee3abb484f2fa5922adc33bb166daf5325d76d/mypy-1.20.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef1415a637cd3627d6304dfbeddbadd21079dafc2a8a753c477ce4fc0c2af54f", size = 13696948, upload-time = "2026-04-13T02:46:15.006Z" }, + { url = "https://files.pythonhosted.org/packages/b2/c6/75e969781c2359b2f9c15b061f28ec6d67c8b61865ceda176e85c8e7f2de/mypy-1.20.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef3461b1ad5cd446e540016e90b5984657edda39f982f4cc45ca317b628f5a37", size = 14706744, upload-time = "2026-04-13T02:46:00.482Z" }, + { url = "https://files.pythonhosted.org/packages/a8/6e/b221b1de981fc4262fe3e0bf9ec272d292dfe42394a689c2d49765c144c4/mypy-1.20.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:542dd63c9e1339b6092eb25bd515f3a32a1453aee8c9521d2ddb17dacd840237", size = 14949035, upload-time = "2026-04-13T02:45:06.021Z" }, + { url = "https://files.pythonhosted.org/packages/ca/4b/298ba2de0aafc0da3ff2288da06884aae7ba6489bc247c933f87847c41b3/mypy-1.20.1-cp312-cp312-win_amd64.whl", hash = "sha256:1d55c7cd8ca22e31f93af2a01160a9e95465b5878de23dba7e48116052f20a8d", size = 10883216, upload-time = "2026-04-13T02:45:47.232Z" }, + { url = "https://files.pythonhosted.org/packages/c7/f9/5e25b8f0b8cb92f080bfed9c21d3279b2a0b6a601cdca369a039ba84789d/mypy-1.20.1-cp312-cp312-win_arm64.whl", hash = "sha256:f5b84a79070586e0d353ee07b719d9d0a4aa7c8ee90c0ea97747e98cbe193019", size = 9814299, upload-time = "2026-04-13T02:45:21.934Z" }, + { url = "https://files.pythonhosted.org/packages/21/e8/ef0991aa24c8f225df10b034f3c2681213cb54cf247623c6dec9a5744e70/mypy-1.20.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f3886c03e40afefd327bd70b3f634b39ea82e87f314edaa4d0cce4b927ddcc1", size = 14500739, upload-time = "2026-04-13T02:46:05.442Z" }, + { url = "https://files.pythonhosted.org/packages/23/73/416ebec3047636ed89fa871dc8c54bf05e9e20aa9499da59790d7adb312d/mypy-1.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e860eb3904f9764e83bafd70c8250bdffdc7dde6b82f486e8156348bf7ceb184", size = 13314735, upload-time = "2026-04-13T02:46:47.154Z" }, + { url = "https://files.pythonhosted.org/packages/10/1e/1505022d9c9ac2e014a384eb17638fb37bf8e9d0a833ea60605b66f8f7ba/mypy-1.20.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a4b5aac6e785719da51a84f5d09e9e843d473170a9045b1ea7ea1af86225df4b", size = 13704356, upload-time = "2026-04-13T02:45:19.773Z" }, + { url = "https://files.pythonhosted.org/packages/98/91/275b01f5eba5c467a3318ec214dd865abb66e9c811231c8587287b92876a/mypy-1.20.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f37b6cd0fe2ad3a20f05ace48ca3523fc52ff86940e34937b439613b6854472e", size = 14696420, upload-time = "2026-04-13T02:45:24.205Z" }, + { url = "https://files.pythonhosted.org/packages/a1/57/b3779e134e1b7250d05f874252780d0a88c068bc054bcff99ca20a3a2986/mypy-1.20.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e4bbb0f6b54ce7cc350ef4a770650d15fa70edd99ad5267e227133eda9c94218", size = 14936093, upload-time = "2026-04-13T02:45:32.087Z" }, + { url = "https://files.pythonhosted.org/packages/be/33/81b64991b0f3f278c3b55c335888794af190b2d59031a5ad1401bcb69f1e/mypy-1.20.1-cp313-cp313-win_amd64.whl", hash = "sha256:c3dc20f8ec76eecd77148cdd2f1542ed496e51e185713bf488a414f862deb8f2", size = 10889659, upload-time = "2026-04-13T02:46:02.926Z" }, + { url = "https://files.pythonhosted.org/packages/1b/fd/7adcb8053572edf5ef8f3db59599dfeeee3be9cc4c8c97e2d28f66f42ac5/mypy-1.20.1-cp313-cp313-win_arm64.whl", hash = "sha256:a9d62bbac5d6d46718e2b0330b25e6264463ed832722b8f7d4440ff1be3ca895", size = 9815515, upload-time = "2026-04-13T02:46:32.103Z" }, + { url = "https://files.pythonhosted.org/packages/40/cd/db831e84c81d57d4886d99feee14e372f64bbec6a9cb1a88a19e243f2ef5/mypy-1.20.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:12927b9c0ed794daedcf1dab055b6c613d9d5659ac511e8d936d96f19c087d12", size = 14483064, upload-time = "2026-04-13T02:45:26.901Z" }, + { url = "https://files.pythonhosted.org/packages/d5/82/74e62e7097fa67da328ac8ece8de09133448c04d20ddeaeba251a3000f01/mypy-1.20.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:752507dd481e958b2c08fc966d3806c962af5a9433b5bf8f3bdd7175c20e34fe", size = 13335694, upload-time = "2026-04-13T02:46:12.514Z" }, + { url = "https://files.pythonhosted.org/packages/74/c4/97e9a0abe4f3cdbbf4d079cb87a03b786efeccf5bf2b89fe4f96939ab2e6/mypy-1.20.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c614655b5a065e56274c6cbbe405f7cf7e96c0654db7ba39bc680238837f7b08", size = 13726365, upload-time = "2026-04-13T02:45:17.422Z" }, + { url = "https://files.pythonhosted.org/packages/d7/aa/a19d884a8d28fcd3c065776323029f204dbc774e70ec9c85eba228b680de/mypy-1.20.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2c3f6221a76f34d5100c6d35b3ef6b947054123c3f8d6938a4ba00b1308aa572", size = 14693472, upload-time = "2026-04-13T02:46:41.253Z" }, + { url = "https://files.pythonhosted.org/packages/84/44/cc9324bd21cf786592b44bf3b5d224b3923c1230ec9898d508d00241d465/mypy-1.20.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4bdfc06303ac06500af71ea0cdbe995c502b3c9ba32f3f8313523c137a25d1b6", size = 14919266, upload-time = "2026-04-13T02:46:28.37Z" }, + { url = "https://files.pythonhosted.org/packages/6e/dc/779abb25a8c63e8f44bf5a336217fa92790fa17e0c40e0c725d10cb01bbd/mypy-1.20.1-cp314-cp314-win_amd64.whl", hash = "sha256:0131edd7eba289973d1ba1003d1a37c426b85cdef76650cd02da6420898a5eb3", size = 11049713, upload-time = "2026-04-13T02:45:57.673Z" }, + { url = "https://files.pythonhosted.org/packages/28/08/4172be2ad7de9119b5a92ca36abbf641afdc5cb1ef4ae0c3a8182f29674f/mypy-1.20.1-cp314-cp314-win_arm64.whl", hash = "sha256:33f02904feb2c07e1fdf7909026206396c9deeb9e6f34d466b4cfedb0aadbbe4", size = 9999819, upload-time = "2026-04-13T02:46:35.039Z" }, + { url = "https://files.pythonhosted.org/packages/2d/af/af9e46b0c8eabbce9fc04a477564170f47a1c22b308822282a59b7ff315f/mypy-1.20.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:168472149dd8cc505c98cefd21ad77e4257ed6022cd5ed2fe2999bed56977a5a", size = 15547508, upload-time = "2026-04-13T02:46:25.588Z" }, + { url = "https://files.pythonhosted.org/packages/a7/cd/39c9e4ad6ba33e069e5837d772a9e6c304b4a5452a14a975d52b36444650/mypy-1.20.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:eb674600309a8f22790cca883a97c90299f948183ebb210fbef6bcee07cb1986", size = 14399557, upload-time = "2026-04-13T02:46:10.021Z" }, + { url = "https://files.pythonhosted.org/packages/83/c1/3fd71bdc118ffc502bf57559c909927bb7e011f327f7bb8e0488e98a5870/mypy-1.20.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef2b2e4cc464ba9795459f2586923abd58a0055487cbe558cb538ea6e6bc142a", size = 15045789, upload-time = "2026-04-13T02:45:10.81Z" }, + { url = "https://files.pythonhosted.org/packages/8e/73/6f07ff8b57a7d7b3e6e5bf34685d17632382395c8bb53364ec331661f83e/mypy-1.20.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dee461d396dd46b3f0ed5a098dbc9b8860c81c46ad44fa071afcfbc149f167c9", size = 15850795, upload-time = "2026-04-13T02:45:03.349Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e2/f7dffec1c7767078f9e9adf0c786d1fe0ff30964a77eb213c09b8b58cb76/mypy-1.20.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e364926308b3e66f1361f81a566fc1b2f8cd47fc8525e8136d4058a65a4b4f02", size = 16088539, upload-time = "2026-04-13T02:46:17.841Z" }, + { url = "https://files.pythonhosted.org/packages/1a/76/e0dee71035316e75a69d73aec2f03c39c21c967b97e277fd0ef8fd6aec66/mypy-1.20.1-cp314-cp314t-win_amd64.whl", hash = "sha256:a0c17fbd746d38c70cbc42647cfd884f845a9708a4b160a8b4f7e70d41f4d7fa", size = 12575567, upload-time = "2026-04-13T02:45:34.795Z" }, + { url = "https://files.pythonhosted.org/packages/22/a8/7ed43c9d9c3d1468f86605e323a5d97e411a448790a00f07e779f3211a46/mypy-1.20.1-cp314-cp314t-win_arm64.whl", hash = "sha256:db2cb89654626a912efda69c0d5c1d22d948265e2069010d3dde3abf751c7d08", size = 10378823, upload-time = "2026-04-13T02:45:13.35Z" }, + { url = "https://files.pythonhosted.org/packages/d8/28/926bd972388e65a39ee98e188ccf67e81beb3aacfd5d6b310051772d974b/mypy-1.20.1-py3-none-any.whl", hash = "sha256:1aae28507f253fe82d883790d1c0a0d35798a810117c88184097fe8881052f06", size = 2636553, upload-time = "2026-04-13T02:46:30.45Z" }, ] [[package]] @@ -1320,7 +1386,7 @@ wheels = [ [[package]] name = "nbconvert" -version = "7.17.0" +version = "7.17.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "beautifulsoup4" }, @@ -1338,9 +1404,9 @@ dependencies = [ { name = "pygments" }, { name = "traitlets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/38/47/81f886b699450d0569f7bc551df2b1673d18df7ff25cc0c21ca36ed8a5ff/nbconvert-7.17.0.tar.gz", hash = "sha256:1b2696f1b5be12309f6c7d707c24af604b87dfaf6d950794c7b07acab96dda78", size = 862855, upload-time = "2026-01-29T16:37:48.478Z" } +sdist = { url = "https://files.pythonhosted.org/packages/01/b1/708e53fe2e429c103c6e6e159106bcf0357ac41aa4c28772bd8402339051/nbconvert-7.17.1.tar.gz", hash = "sha256:34d0d0a7e73ce3cbab6c5aae8f4f468797280b01fd8bd2ca746da8569eddd7d2", size = 865311, upload-time = "2026-04-08T00:44:14.914Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0d/4b/8d5f796a792f8a25f6925a96032f098789f448571eb92011df1ae59e8ea8/nbconvert-7.17.0-py3-none-any.whl", hash = "sha256:4f99a63b337b9a23504347afdab24a11faa7d86b405e5c8f9881cd313336d518", size = 261510, upload-time = "2026-01-29T16:37:46.322Z" }, + { url = "https://files.pythonhosted.org/packages/67/f8/bb0a9d5f46819c821dc1f004aa2cc29b1d91453297dbf5ff20470f00f193/nbconvert-7.17.1-py3-none-any.whl", hash = "sha256:aa85c087b435e7bf1ffd03319f658e285f2b89eccab33bc1ba7025495ab3e7c8", size = 261927, upload-time = "2026-04-08T00:44:12.845Z" }, ] [[package]] @@ -1360,7 +1426,7 @@ wheels = [ [[package]] name = "nblite" -version = "1.2.0" +version = "1.2.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jinja2" }, @@ -1372,9 +1438,9 @@ dependencies = [ { name = "rich" }, { name = "typer" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c4/80/bb4374bfd98c9a8f4071ff415eae07c48fb57d686e74bf9835cd57196ff9/nblite-1.2.0.tar.gz", hash = "sha256:71514c9eea2737f5397def1a64663a3363477176eb8d512390fe096d1a7e673f", size = 324937, upload-time = "2026-02-20T11:41:03.101Z" } +sdist = { url = "https://files.pythonhosted.org/packages/80/a5/e7a91b57e1dc17d0e546e95500388b957320588a563c20c053eb593b3353/nblite-1.2.1.tar.gz", hash = "sha256:0f5b129d93069f0f7502ddcc1f8d960af78eefb9d097b1b85bad088ec1618cbb", size = 325768, upload-time = "2026-03-15T10:50:07.001Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ab/69/e7496d03514ecbe0b0ed176179e02453364b379aa1aca818a423890a1f7f/nblite-1.2.0-py3-none-any.whl", hash = "sha256:50509ca9d07a78cc356029565ac24150232b7b71530ca2efbef220f6f9f3521a", size = 104733, upload-time = "2026-02-20T11:41:01.371Z" }, + { url = "https://files.pythonhosted.org/packages/27/4d/471d79351bab69bf96f12d3b689d430342857138bb45fb82cb08a19125d1/nblite-1.2.1-py3-none-any.whl", hash = "sha256:04083f525229f8d8445c08dc192286491ec63e730806c84faf226b9d33b35ca0", size = 105264, upload-time = "2026-03-15T10:50:05.235Z" }, ] [[package]] @@ -1388,7 +1454,7 @@ wheels = [ [[package]] name = "netrun" -version = "0.4.1" +version = "0.5.0" source = { editable = "." } dependencies = [ { name = "beartype" }, @@ -1400,10 +1466,6 @@ dependencies = [ ] [package.optional-dependencies] -cli = [ - { name = "tomli-w" }, - { name = "typer" }, -] deploy = [ { name = "pyinfra" }, ] @@ -1418,7 +1480,7 @@ dev = [ { name = "jupyterlab" }, { name = "mypy" }, { name = "nblite" }, - { name = "netrun", extra = ["cli"] }, + { name = "netrun-cli" }, { name = "pytest" }, { name = "pytest-asyncio" }, { name = "ruff" }, @@ -1433,12 +1495,10 @@ requires-dist = [ { name = "nblite", specifier = ">=1.1.1" }, { name = "netrun-sim", editable = "../netrun-sim/python" }, { name = "pyinfra", marker = "extra == 'deploy'", specifier = ">=3.0" }, - { name = "tomli-w", marker = "extra == 'cli'", specifier = ">=1.0" }, - { name = "typer", marker = "extra == 'cli'", specifier = ">=0.15.0" }, { name = "websockets", specifier = ">=16.0" }, { name = "xxhash", specifier = ">=3.6.0" }, ] -provides-extras = ["deploy", "cli", "serialization"] +provides-extras = ["deploy", "serialization"] [package.metadata.requires-dev] dev = [ @@ -1446,15 +1506,39 @@ dev = [ { name = "jupyterlab", specifier = ">=4.5.1" }, { name = "mypy", specifier = ">=1.19.1" }, { name = "nblite", specifier = ">=1.1.5" }, - { name = "netrun", extras = ["cli"] }, + { name = "netrun-cli", editable = "../netrun-cli" }, { name = "pytest", specifier = ">=9.0.2" }, { name = "pytest-asyncio", specifier = ">=1.3.0" }, { name = "ruff", specifier = ">=0.8.0" }, ] +[[package]] +name = "netrun-cli" +version = "0.5.0" +source = { editable = "../netrun-cli" } +dependencies = [ + { name = "netrun" }, + { name = "tomli-w" }, + { name = "typer" }, +] + +[package.metadata] +requires-dist = [ + { name = "netrun", editable = "." }, + { name = "tomli-w", specifier = ">=1.0" }, + { name = "typer", specifier = ">=0.15.0" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "nblite", specifier = ">=1.1.5" }, + { name = "pytest", specifier = ">=9.0.2" }, + { name = "ruff", specifier = ">=0.8.0" }, +] + [[package]] name = "netrun-sim" -version = "0.1.4" +version = "0.2.0" source = { editable = "../netrun-sim/python" } dependencies = [ { name = "python-ulid" }, @@ -1511,11 +1595,11 @@ wheels = [ [[package]] name = "packaging" -version = "26.0" +version = "26.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +sdist = { url = "https://files.pythonhosted.org/packages/df/de/0d2b39fb4af88a0258f3bac87dfcbb48e73fbdea4a2ed0e2213f9a4c2f9a/packaging-26.1.tar.gz", hash = "sha256:f042152b681c4bfac5cae2742a55e103d27ab2ec0f3d88037136b6bfe7c9c5de", size = 215519, upload-time = "2026-04-14T21:12:49.362Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, + { url = "https://files.pythonhosted.org/packages/7a/c2/920ef838e2f0028c8262f16101ec09ebd5969864e5a64c4c05fad0617c56/packaging-26.1-py3-none-any.whl", hash = "sha256:5d9c0669c6285e491e0ced2eee587eaf67b670d94a19e94e3984a481aba6802f", size = 95831, upload-time = "2026-04-14T21:12:47.56Z" }, ] [[package]] @@ -1573,11 +1657,11 @@ wheels = [ [[package]] name = "platformdirs" -version = "4.9.2" +version = "4.9.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1b/04/fea538adf7dbbd6d186f551d595961e564a3b6715bdf276b477460858672/platformdirs-4.9.2.tar.gz", hash = "sha256:9a33809944b9db043ad67ca0db94b14bf452cc6aeaac46a88ea55b26e2e9d291", size = 28394, upload-time = "2026-02-16T03:56:10.574Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9f/4a/0883b8e3802965322523f0b200ecf33d31f10991d0401162f4b23c698b42/platformdirs-4.9.6.tar.gz", hash = "sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a", size = 29400, upload-time = "2026-04-09T00:04:10.812Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/48/31/05e764397056194206169869b50cf2fee4dbbbc71b344705b9c0d878d4d8/platformdirs-4.9.2-py3-none-any.whl", hash = "sha256:9170634f126f8efdae22fb58ae8a0eaa86f38365bc57897a6c4f781d1f5875bd", size = 21168, upload-time = "2026-02-16T03:56:08.891Z" }, + { url = "https://files.pythonhosted.org/packages/75/a6/a0a304dc33b49145b21f4808d763822111e67d1c3a32b524a1baf947b6e1/platformdirs-4.9.6-py3-none-any.whl", hash = "sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917", size = 21348, upload-time = "2026-04-09T00:04:09.463Z" }, ] [[package]] @@ -1591,11 +1675,11 @@ wheels = [ [[package]] name = "prometheus-client" -version = "0.24.1" +version = "0.25.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f0/58/a794d23feb6b00fc0c72787d7e87d872a6730dd9ed7c7b3e954637d8f280/prometheus_client-0.24.1.tar.gz", hash = "sha256:7e0ced7fbbd40f7b84962d5d2ab6f17ef88a72504dcf7c0b40737b43b2a461f9", size = 85616, upload-time = "2026-01-14T15:26:26.965Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/fb/d9aa83ffe43ce1f19e557c0971d04b90561b0cfd50762aafb01968285553/prometheus_client-0.25.0.tar.gz", hash = "sha256:5e373b75c31afb3c86f1a52fa1ad470c9aace18082d39ec0d2f918d11cc9ba28", size = 86035, upload-time = "2026-04-09T19:53:42.359Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/74/c3/24a2f845e3917201628ecaba4f18bab4d18a337834c1df2a159ee9d22a42/prometheus_client-0.24.1-py3-none-any.whl", hash = "sha256:150db128af71a5c2482b36e588fc8a6b95e498750da4b17065947c16070f4055", size = 64057, upload-time = "2026-01-14T15:26:24.42Z" }, + { url = "https://files.pythonhosted.org/packages/8d/9b/d4b1e644385499c8346fa9b622a3f030dce14cd6ef8a1871c221a17a67e7/prometheus_client-0.25.0-py3-none-any.whl", hash = "sha256:d5aec89e349a6ec230805d0df882f3807f74fd6c1a2fa86864e3c2279059fed1", size = 64154, upload-time = "2026-04-09T19:53:41.324Z" }, ] [[package]] @@ -1667,7 +1751,7 @@ wheels = [ [[package]] name = "pydantic" -version = "2.12.5" +version = "2.13.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, @@ -1675,120 +1759,125 @@ dependencies = [ { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } +sdist = { url = "https://files.pythonhosted.org/packages/09/e5/06d23afac9973109d1e3c8ad38e1547a12e860610e327c05ee686827dc37/pydantic-2.13.2.tar.gz", hash = "sha256:b418196607e61081c3226dcd4f0672f2a194828abb9109e9cfb84026564df2d1", size = 843836, upload-time = "2026-04-17T09:31:59.636Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, + { url = "https://files.pythonhosted.org/packages/77/ca/b45c378e6e8d0b90577288b533e04e95b7afd61bb1d51b6c263176435489/pydantic-2.13.2-py3-none-any.whl", hash = "sha256:a525087f4c03d7e7456a3de89b64cd693d2229933bb1068b9af6befd5563694e", size = 471947, upload-time = "2026-04-17T09:31:57.541Z" }, ] [[package]] name = "pydantic-core" -version = "2.41.5" +version = "2.46.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" }, - { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" }, - { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" }, - { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" }, - { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" }, - { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" }, - { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" }, - { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" }, - { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" }, - { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" }, - { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" }, - { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" }, - { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" }, - { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" }, - { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, - { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, - { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, - { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, - { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, - { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, - { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, - { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, - { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, - { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, - { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, - { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, - { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, - { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, - { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, - { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, - { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, - { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, - { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, - { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, - { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, - { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, - { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, - { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, - { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, - { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, - { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, - { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, - { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, - { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, - { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, - { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, - { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, - { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, - { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, - { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, - { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, - { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, - { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, - { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, - { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, - { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, - { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, - { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, - { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, - { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, - { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, - { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, - { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, - { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, - { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, - { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, - { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, - { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, - { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, - { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, - { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" }, - { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" }, - { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" }, - { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" }, - { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, - { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, - { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, - { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, - { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" }, - { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" }, - { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" }, - { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" }, - { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" }, - { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" }, - { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" }, - { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/43/bb/4742f05b739b2478459bb16fa8470549518c802e06ddcf3f106c5081315e/pydantic_core-2.46.2.tar.gz", hash = "sha256:37bb079f9ee3f1a519392b73fda2a96379b31f2013c6b467fe693e7f2987f596", size = 471269, upload-time = "2026-04-17T09:10:07.017Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/91/089f517a725f29084364169437833ab0ae4da4d7a6ed9d4474db7f1412e6/pydantic_core-2.46.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d8060f42db3cd204871db0afd51fef54a13fa544c4dd48cdcae2e174ef40c8ba", size = 2106218, upload-time = "2026-04-17T09:10:48.023Z" }, + { url = "https://files.pythonhosted.org/packages/a0/92/23858ed1b58f2a134e50c2fdd0e34ea72721ccb257e1e9346514e1ccb5b9/pydantic_core-2.46.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:73a9d2809bd8d4a7cda4d336dc996a565eb4feaaa39932f9d85a65fa18382f28", size = 1948087, upload-time = "2026-04-17T09:11:58.639Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ac/e2240fccb4794e965817593d5a46cf5ea22f2001b73fe360b7578925b7d8/pydantic_core-2.46.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b0a2dee92dfaabcfb93629188c3e9cf74fdfc0f22e7c369cb444a98814a1e50", size = 1972931, upload-time = "2026-04-17T09:13:13.304Z" }, + { url = "https://files.pythonhosted.org/packages/1a/da/3b11dab2aa15c5c8ed20a01eb7aa432a78b8e3a4713659f7e58490a020a5/pydantic_core-2.46.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3098446ba8cf774f61cb8d4008c1dba14a30426a15169cd95ac3392a461193b1", size = 2040454, upload-time = "2026-04-17T09:13:47.895Z" }, + { url = "https://files.pythonhosted.org/packages/d7/39/c4cf5e1f1c6c34c53c0902039c95d81dc15cdd1f03634bd1a93f33e70a72/pydantic_core-2.46.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:57c584af6c375ea3f826d8131a94cb212b3d9926eaff67117e3711bbff3a83a5", size = 2221320, upload-time = "2026-04-17T09:13:08.568Z" }, + { url = "https://files.pythonhosted.org/packages/c7/46/891035bc9e93538e754c3188424d24b5a69ec3ae5210fa01d483e99b3302/pydantic_core-2.46.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:547381cca999be88b4715a0ed7afa11f07fc7e53cb1883687b190d25a92c56cf", size = 2274559, upload-time = "2026-04-17T09:11:10.257Z" }, + { url = "https://files.pythonhosted.org/packages/ab/d0/7af0b905b3148152c159c9caf203e7ecd9b90b76389f0862e6ab0cf1b2a3/pydantic_core-2.46.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:caeed15dcb1233a5a94bc6ff37ef5393cf5b33a45e4bdfb2d6042f3d24e1cb27", size = 2089239, upload-time = "2026-04-17T09:13:06.326Z" }, + { url = "https://files.pythonhosted.org/packages/c5/bc/566afe02ba2de37712eece74ac7bfba322abd7916410bf90504f1b17ddad/pydantic_core-2.46.2-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:c05f53362568c75476b5c96659377a5dfd982cfbe5a5c07de5106d08a04efc4f", size = 2116182, upload-time = "2026-04-17T09:11:33.738Z" }, + { url = "https://files.pythonhosted.org/packages/4e/5b/3fcb3a229bbfa23b0e3c65014057af0f9d51ec7a2d9f7adb282f41ff5ac8/pydantic_core-2.46.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2643ac7eae296200dbd48762a1c852cf2cad5f5e3eba34e652053cebf03becf8", size = 2172346, upload-time = "2026-04-17T09:10:46.472Z" }, + { url = "https://files.pythonhosted.org/packages/43/9a/baa9e3aa70ea7bbcb9db0f87162a371649ac80c03e43eb54af193390cf17/pydantic_core-2.46.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:dc4620a47c6fe6a39f89392c00833a82fc050ce90169798f78a25a8d4df03b6e", size = 2179540, upload-time = "2026-04-17T09:11:21.881Z" }, + { url = "https://files.pythonhosted.org/packages/bd/46/912047a5427f949c909495704b3c8b9ead9d1c66f87e96606011beab1fcb/pydantic_core-2.46.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:78cb0d2453b50bf2035f85fd0d9cfabdb98c47f9c53ddb7c23873cd83da9560b", size = 2327423, upload-time = "2026-04-17T09:13:40.291Z" }, + { url = "https://files.pythonhosted.org/packages/e9/bf/c5e661451dc9411c2ab88a244c1ba57644950c971486040dc200f77b69f4/pydantic_core-2.46.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f0c1cbb7d6112932cc188c6be007a5e2867005a069e47f42fe67bf5f122b0908", size = 2348652, upload-time = "2026-04-17T09:10:37.76Z" }, + { url = "https://files.pythonhosted.org/packages/77/b3/3219e7c522af54b010cf7422dcb11cc6616a4414d1ccd628b0d3f61c6af6/pydantic_core-2.46.2-cp311-cp311-win32.whl", hash = "sha256:c1ce5b2366f85cfdbf7f0907755043707f86d09a5b1b1acebbb7bf1600d75c64", size = 1974410, upload-time = "2026-04-17T09:13:27.392Z" }, + { url = "https://files.pythonhosted.org/packages/e5/29/e5cfac8a74c59873dfd47d3a1477c39ad9247639a7120d3e251a9ff12417/pydantic_core-2.46.2-cp311-cp311-win_amd64.whl", hash = "sha256:f1a6197eadff5bd0bb932f12bb038d403cb75db5b0b391e70e816a647745ddaf", size = 2071158, upload-time = "2026-04-17T09:09:57.69Z" }, + { url = "https://files.pythonhosted.org/packages/6f/8b/b7b19b717cdb3675cb109de143f62d4dc62f5d4a0b9879b6f1ace62c6654/pydantic_core-2.46.2-cp311-cp311-win_arm64.whl", hash = "sha256:15e42885b283f87846ee79e161002c5c496ef747a73f6e47054f45a13d9035bc", size = 2043507, upload-time = "2026-04-17T09:09:51.828Z" }, + { url = "https://files.pythonhosted.org/packages/97/ec/2fafa4c86f5d2a69372c7cddef30925fd0e370b1efaf556609c1a0196d8a/pydantic_core-2.46.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:ea1ad8c89da31512fe2d249cf0638fb666925bda341901541bc5f3311c6fcc9e", size = 2101729, upload-time = "2026-04-17T09:12:30.042Z" }, + { url = "https://files.pythonhosted.org/packages/cf/55/be5386c2c4b49af346e8a26b748194ff25757bbb6cf544130854e997af7a/pydantic_core-2.46.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b308da17b92481e0587244631c5529e5d91d04cb2b08194825627b1eca28e21e", size = 1951546, upload-time = "2026-04-17T09:10:10.585Z" }, + { url = "https://files.pythonhosted.org/packages/29/92/89e273a055ce440e6636c756379af35ad86da9d336a560049c3ba5e41c80/pydantic_core-2.46.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d333a50bdd814a917d8d6a7ee35ba2395d53ddaa882613bc24e54a9d8b129095", size = 1976178, upload-time = "2026-04-17T09:11:49.619Z" }, + { url = "https://files.pythonhosted.org/packages/91/b3/e4664469cf70c0cb0f7b2f5719d64e5968bb6f38217042c2afa3d3c4ba17/pydantic_core-2.46.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1d00b99590c5bd1fabbc5d28b170923e32c1b1071b1f1de1851a4d14d89eb192", size = 2051697, upload-time = "2026-04-17T09:12:04.917Z" }, + { url = "https://files.pythonhosted.org/packages/98/58/dbf68213ee06ce51cdd6d8c95f97980e646858c45bd96bd2dfb40433be73/pydantic_core-2.46.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9f0e686960ffe9e65066395af856ac2d52c159043144433602c50c221d81c1ba", size = 2233160, upload-time = "2026-04-17T09:12:00.956Z" }, + { url = "https://files.pythonhosted.org/packages/f5/d3/68092aa0ee6c60ff4de4740eb82db3d4ce338ec89b3cecb978c532472f12/pydantic_core-2.46.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d1128da41c9cb474e0a4701f9c363ec645c9d1a02229904c76bf4e0a194fde2", size = 2298398, upload-time = "2026-04-17T09:10:29.694Z" }, + { url = "https://files.pythonhosted.org/packages/e4/51/5d6155eb737db55b0ad354ca5f333ef009f75feb67df2d79a84bace45af6/pydantic_core-2.46.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48649cf2d8c358d79586e9fb2f8235902fcaa2d969ec1c5301f2d1873b2f8321", size = 2094058, upload-time = "2026-04-17T09:12:10.995Z" }, + { url = "https://files.pythonhosted.org/packages/6b/f3/eb4a986197d71319430464ff181226c95adc8f06d932189b158bae5a82f5/pydantic_core-2.46.2-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:b902f0fc7c2cf503865a05718b68147c6cd5d0a3867af38c527be574a9fa6e9d", size = 2130388, upload-time = "2026-04-17T09:12:41.159Z" }, + { url = "https://files.pythonhosted.org/packages/56/00/44a9c4fe6d0f64b5786d6a8c649d6f0e34ba6c89b3663add1066e54451a2/pydantic_core-2.46.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e80011f808b03d1d87a8f1e76ae3da19a18eb706c823e17981dcf1fae43744fc", size = 2184245, upload-time = "2026-04-17T09:12:36.532Z" }, + { url = "https://files.pythonhosted.org/packages/78/6b/685b98a834d5e3d1c34a1bde1627525559dd223b75075bc7490cdb24eb33/pydantic_core-2.46.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b839d5c802e31348b949b6473f8190cddbf7d47475856d8ac995a373ee16ec59", size = 2186842, upload-time = "2026-04-17T09:13:04.054Z" }, + { url = "https://files.pythonhosted.org/packages/22/64/caa2f5a2ac8b6113adaa410ccdf31ba7f54897a6e54cd0d726fc7e780c88/pydantic_core-2.46.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:c6b1064f3f9cf9072e1d59dd2936f9f3b668bec1c37039708c9222db703c0d5b", size = 2336066, upload-time = "2026-04-17T09:12:13.006Z" }, + { url = "https://files.pythonhosted.org/packages/ee/f9/7d2701bf82945b5b9e7df8347be97ef6a36da2846bfe5b4afec299ffe27b/pydantic_core-2.46.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:37a68e6f2ac95578ce3c0564802404b27b24988649616e556c07e77111ed3f1d", size = 2363691, upload-time = "2026-04-17T09:13:42.972Z" }, + { url = "https://files.pythonhosted.org/packages/3b/65/0dab11574101522941055109419db3cc09db871643dc3fc74e2413215e5b/pydantic_core-2.46.2-cp312-cp312-win32.whl", hash = "sha256:d9ffa75a7ef4b97d6e5e205fabd4304ef01fec09e6f1bdde04b9ad1b07d20289", size = 1958801, upload-time = "2026-04-17T09:11:31.981Z" }, + { url = "https://files.pythonhosted.org/packages/13/2b/df84baa609c676f6450b8ecad44ea59146c805e3371b7b52443c0899f989/pydantic_core-2.46.2-cp312-cp312-win_amd64.whl", hash = "sha256:0551f2d2ddb68af5a00e26497f8025c538f73ef3cb698f8e5a487042cd2792a8", size = 2072634, upload-time = "2026-04-17T09:11:02.407Z" }, + { url = "https://files.pythonhosted.org/packages/d1/4e/e1ce8029fc438086a946739bf9d596f70ff470aad4a8345555920618cabe/pydantic_core-2.46.2-cp312-cp312-win_arm64.whl", hash = "sha256:83aef30f106edcc21a6a4cc44b82d3169a1dbe255508db788e778f3c804d3583", size = 2026188, upload-time = "2026-04-17T09:13:11.083Z" }, + { url = "https://files.pythonhosted.org/packages/07/2b/662e48254479a2d3450ba24b1e25061108b64339794232f503990c519144/pydantic_core-2.46.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:d26e9eea3715008a09a74585fe9becd0c67fbb145dc4df9756d597d7230a652c", size = 2101762, upload-time = "2026-04-17T09:10:13.87Z" }, + { url = "https://files.pythonhosted.org/packages/73/ab/bafd7c7503757ccc8ec4d1911e106fe474c629443648c51a88f08b0fe91a/pydantic_core-2.46.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:48b36e3235140510dc7861f0cd58b714b1cdd3d48f75e10ce52e69866b746f10", size = 1951814, upload-time = "2026-04-17T09:12:25.934Z" }, + { url = "https://files.pythonhosted.org/packages/92/cc/7549c2d57ba2e9a42caa5861a2d398dbe31c02c6aca783253ace59ce84f8/pydantic_core-2.46.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:36b1f99dc451f1a3981f236151465bcf995bbe712d0727c9f7b236fe228a8133", size = 1977329, upload-time = "2026-04-17T09:13:37.605Z" }, + { url = "https://files.pythonhosted.org/packages/18/50/7ed4a8a0d478a4dca8f0134a5efa7193f03cc8520dd4c9509339fb2e5002/pydantic_core-2.46.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8641c8d535c2d95b45c2e19b646ecd23ebba35d461e0ae48a3498277006250ab", size = 2051832, upload-time = "2026-04-17T09:12:49.771Z" }, + { url = "https://files.pythonhosted.org/packages/dc/16/bb35b193741c0298ddc5f5e4234269efdc0c65e2bcd198aa0de9b68845e4/pydantic_core-2.46.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:20fb194788a0a50993e87013e693494ba183a2af5b44e99cf060bbae10912b11", size = 2233127, upload-time = "2026-04-17T09:11:04.449Z" }, + { url = "https://files.pythonhosted.org/packages/91/a5/98f4b637149185addea19e1785ea20c373cca31b202f589111d8209d9873/pydantic_core-2.46.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9262d11d0cd11ee3303a95156939402bed6cedfe5ed0e331b95a283a4da6eb8b", size = 2297418, upload-time = "2026-04-17T09:11:25.929Z" }, + { url = "https://files.pythonhosted.org/packages/36/90/93a5d21990b152da7b7507b7fddb0b935f6a0984d57ac3ec45a6e17777a2/pydantic_core-2.46.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac204542736aa295fa25f713b7fad6fc50b46ab7764d16087575c85f085174f3", size = 2093735, upload-time = "2026-04-17T09:12:06.908Z" }, + { url = "https://files.pythonhosted.org/packages/14/22/b8b1ffdddf08b4e84380bcb67f41dbbf4c171377c1d36fc6290794bb2094/pydantic_core-2.46.2-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9a7c43a0584742dface3ca0daf6f719d46c1ac2f87cf080050f9ae052c75e1b2", size = 2127570, upload-time = "2026-04-17T09:11:53.906Z" }, + { url = "https://files.pythonhosted.org/packages/c6/26/e60d72b4e2d0ce1fa811044a974412ac1c567fe067d97b3e6b290530786e/pydantic_core-2.46.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fd05e1edb6a90ad446fa268ab09e59202766b837597b714b2492db11ee87fab9", size = 2183524, upload-time = "2026-04-17T09:11:30.092Z" }, + { url = "https://files.pythonhosted.org/packages/35/32/36bec7584a1eefb17dec4dfa1c946d3fe4440f466c5705b8adfda69c9a9f/pydantic_core-2.46.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:91155b110788b5501abc7ea954f1d08606219e4e28e3c73a94124307c06efb80", size = 2185408, upload-time = "2026-04-17T09:10:57.228Z" }, + { url = "https://files.pythonhosted.org/packages/fc/d6/1a5689d873620efd67d6b163db0c444c056adb0849b5bc33e2b9f09665a6/pydantic_core-2.46.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:e4e2c72a529fa03ff228be1d2b76944013f428220b764e03cc50ada67e17a42c", size = 2335171, upload-time = "2026-04-17T09:11:43.369Z" }, + { url = "https://files.pythonhosted.org/packages/3e/8e/675104802abe8ef502b072050ee5f2e915251aa1a3af87e1015ce31ec42d/pydantic_core-2.46.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:56291ec1a11c3499890c99a8fd9053b47e60fe837a77ec72c0671b1b8b3dce24", size = 2362743, upload-time = "2026-04-17T09:10:18.333Z" }, + { url = "https://files.pythonhosted.org/packages/8d/bc/86c5dde4fa6e24467680eef5047da3c1a19be0a527d0d8e14aa76b39307c/pydantic_core-2.46.2-cp313-cp313-win32.whl", hash = "sha256:b50f9c5f826ddca1246f055148df939f5f3f2d0d96db73de28e2233f22210d4c", size = 1958074, upload-time = "2026-04-17T09:12:38.622Z" }, + { url = "https://files.pythonhosted.org/packages/2a/97/2537e8c1282b2c4eb062580c0d7a4339e10b072b803d1ee0b7f1f0a5c22c/pydantic_core-2.46.2-cp313-cp313-win_amd64.whl", hash = "sha256:251a57788823230ca8cbc99e6245d1a2ed6e180ec4864f251c94182c580c7f2e", size = 2071741, upload-time = "2026-04-17T09:13:32.405Z" }, + { url = "https://files.pythonhosted.org/packages/da/aa/2ee75798706f9dbc4e76dbe59e41a396c5c311e3d6223b9cf6a5fa7780be/pydantic_core-2.46.2-cp313-cp313-win_arm64.whl", hash = "sha256:315d32d1a71494d6b4e1e14a9fa7a4329597b4c4340088ad7e1a9dafbeed92a9", size = 2025955, upload-time = "2026-04-17T09:10:15.567Z" }, + { url = "https://files.pythonhosted.org/packages/d0/96/a50ccb6b539ae780f73cea74905468777680e30c6c3bdf714b9d4c116ea0/pydantic_core-2.46.2-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:4f59b45f3ef8650c0c736a57f59031d47ed9df4c0a64e83796849d7d14863a2d", size = 2097111, upload-time = "2026-04-17T09:10:49.617Z" }, + { url = "https://files.pythonhosted.org/packages/34/5f/fdead7b3afa822ab6e5a18ee0ecffd54937de1877c01ed13a342e0fb3f07/pydantic_core-2.46.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3a075a29ebef752784a91532a1a85be6b234ccffec0a9d7978a92696387c3da6", size = 1951904, upload-time = "2026-04-17T09:12:32.062Z" }, + { url = "https://files.pythonhosted.org/packages/95/e0/1c5d547e550cdab1bec737492aa08865337af6fe7fc9b96f7f45f17d9519/pydantic_core-2.46.2-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d12d786e30c04a9d307c5d7080bf720d9bac7f1668191d8e37633a9562749e2", size = 1978667, upload-time = "2026-04-17T09:11:35.589Z" }, + { url = "https://files.pythonhosted.org/packages/0e/cb/665ce629e218c8228302cb94beff4f6531082a2c87d3ecc3d5e63a26f392/pydantic_core-2.46.2-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0d5e6d6343b0b5dcacb3503b5de90022968da8ed0ab9ab39d3eda71c20cbf84e", size = 2046721, upload-time = "2026-04-17T09:11:47.725Z" }, + { url = "https://files.pythonhosted.org/packages/77/e9/6cb2cf60f54c1472bbdfce19d957553b43dbba79d1d7b2930a195c594785/pydantic_core-2.46.2-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:233eebac0999b6b9ba76eb56f3ec8fce13164aa16b6d2225a36a79e0f95b5973", size = 2228483, upload-time = "2026-04-17T09:12:08.837Z" }, + { url = "https://files.pythonhosted.org/packages/0d/2a/93e018dd5571f781ebaeda8c0cf65398489d5bee9b1f484df0b6149b43b9/pydantic_core-2.46.2-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9cc0eee720dd2f14f3b7c349469402b99ad81a174ab49d3533974529e9d93992", size = 2294663, upload-time = "2026-04-17T09:12:52.053Z" }, + { url = "https://files.pythonhosted.org/packages/5e/4f/49e57ca55c770c93d9bb046666a54949b42e3c9099a0c5fe94557873fe30/pydantic_core-2.46.2-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83ee76bf2c9910513dbc19e7d82367131fa7508dedd6186a462393071cc11059", size = 2098742, upload-time = "2026-04-17T09:13:45.472Z" }, + { url = "https://files.pythonhosted.org/packages/c6/b0/6e46b5cd3332af665f794b8cdeea206618a8630bd9e7bcc36864518fce81/pydantic_core-2.46.2-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:d61db38eb4ee5192f0c261b7f2d38e420b554df8912245e3546aee5c45e2fd78", size = 2125922, upload-time = "2026-04-17T09:12:54.304Z" }, + { url = "https://files.pythonhosted.org/packages/06/d1/40850c81585be443a2abfdf7f795f8fae831baf8e2f9b2133c8246ac671c/pydantic_core-2.46.2-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8f09a713d17bcd55da8ab02ebd9110c5246a49c44182af213b5212800af8bc83", size = 2183000, upload-time = "2026-04-17T09:10:59.027Z" }, + { url = "https://files.pythonhosted.org/packages/04/af/8493d7dfa03ebb7866909e577c6aa65ea0de7377b86023cc51d0c8e11db3/pydantic_core-2.46.2-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:30cacc5fb696e64b8ef6fd31d9549d394dd7d52760db072eecb98e37e3af1677", size = 2180335, upload-time = "2026-04-17T09:12:57.01Z" }, + { url = "https://files.pythonhosted.org/packages/72/5b/1f6a344c4ffdf284da41c6067b82d5ebcbd11ce1b515ae4b662d4adb6f61/pydantic_core-2.46.2-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:7ccfb105fcfe91a22bbb5563ad3dc124bc1aa75bfd2e53a780ab05f78cdf6108", size = 2330002, upload-time = "2026-04-17T09:12:02.958Z" }, + { url = "https://files.pythonhosted.org/packages/25/ff/9a694126c12d6d2f48a0cafa6f8eef88ef0d8825600e18d03ff2e896c3b2/pydantic_core-2.46.2-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:13ffef637dc8370c249e5b26bd18e9a80a4fca3d809618c44e18ec834a7ca7a8", size = 2359920, upload-time = "2026-04-17T09:10:27.764Z" }, + { url = "https://files.pythonhosted.org/packages/51/c8/3a35c763d68a9cb2675eb10ef242cf66c5d4701b28ae12e688d67d2c180e/pydantic_core-2.46.2-cp314-cp314-win32.whl", hash = "sha256:1b0ab6d756ca2704a938e6c31b53f290c2f9c10d3914235410302a149de1a83e", size = 1953701, upload-time = "2026-04-17T09:13:30.021Z" }, + { url = "https://files.pythonhosted.org/packages/1a/6a/f2726a780365f7dfd89d62036f984f7acb99978c60c5e1fa7c0cb898ed11/pydantic_core-2.46.2-cp314-cp314-win_amd64.whl", hash = "sha256:99ebade8c9ada4df975372d8dd25883daa0e379a05f1cd0c99aa0c04368d01a6", size = 2071867, upload-time = "2026-04-17T09:10:39.205Z" }, + { url = "https://files.pythonhosted.org/packages/e1/79/76baacb9feba3d7c399b245ca1a29c74ea0db04ea693811374827eec2290/pydantic_core-2.46.2-cp314-cp314-win_arm64.whl", hash = "sha256:de87422197cf7f83db91d89c86a21660d749b3cd76cd8a45d115b8e675670f02", size = 2017252, upload-time = "2026-04-17T09:10:26.175Z" }, + { url = "https://files.pythonhosted.org/packages/f1/3b/77c26938f817668d9ad9bab1a905cb23f11d9a3d4bf724d429b3e55a8eaf/pydantic_core-2.46.2-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:236f22b4a206b5b61db955396b7cf9e2e1ff77f372efe9570128ccfcd6a525eb", size = 2094545, upload-time = "2026-04-17T09:12:19.339Z" }, + { url = "https://files.pythonhosted.org/packages/fe/de/42c13f590e3c260966aa49bcdb1674774f975467c49abd51191e502bea28/pydantic_core-2.46.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c2012f64d2cd7cca50f49f22445aa5a88691ac2b4498ee0a9a977f8ca4f7289f", size = 1933953, upload-time = "2026-04-17T09:09:55.889Z" }, + { url = "https://files.pythonhosted.org/packages/4e/84/ebe3ebb3e2d8db656937cfa6f97f544cb7132f2307a4a7dfdcd0ea102a12/pydantic_core-2.46.2-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d07d6c63106d3a9c9a333e2636f9c82c703b1a9e3b079299e58747964e4fdb72", size = 1974435, upload-time = "2026-04-17T09:10:12.371Z" }, + { url = "https://files.pythonhosted.org/packages/b9/15/0bf51ca6709477cd4ef86148b6d7844f3308f029eac361dd0383f1e17b1a/pydantic_core-2.46.2-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c326a2b4b85e959d9a1fc3a11f32f84611b6ec07c053e1828a860edf8d068208", size = 2031113, upload-time = "2026-04-17T09:10:00.752Z" }, + { url = "https://files.pythonhosted.org/packages/02/ae/b7b5af9b79db036d9e61a44c481c17a213dc8fc4b8b71fe6875a72fc778b/pydantic_core-2.46.2-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac8a65e798f2462552c00d2e013d532c94d646729dda98458beaf51f9ec7b120", size = 2236325, upload-time = "2026-04-17T09:10:33.227Z" }, + { url = "https://files.pythonhosted.org/packages/a6/ae/ecef7477b5a03d4a499708f7e75d2836452ebb70b776c2d64612b334f57a/pydantic_core-2.46.2-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5a3c2bc1cc8164bedbc160b7bb1e8cc1e8b9c27f69ae4f9ae2b976cdae02b2dd", size = 2278135, upload-time = "2026-04-17T09:10:23.287Z" }, + { url = "https://files.pythonhosted.org/packages/db/e4/2f9d82faa47af6c39fc3f120145fd915971e1e0cb6b55b494fad9fdf8275/pydantic_core-2.46.2-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e69aa5e10b7e8b1bb4a6888650fd12fcbf11d396ca11d4a44de1450875702830", size = 2109071, upload-time = "2026-04-17T09:11:06.149Z" }, + { url = "https://files.pythonhosted.org/packages/f1/9c/677cf10873fbd0b116575ab7b97c90482b21564f8a8040beb18edef7a577/pydantic_core-2.46.2-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:4e6df5c3301e65fb42bc5338bf9a1027a02b0a31dc7f54c33775229af474daf0", size = 2106028, upload-time = "2026-04-17T09:10:51.525Z" }, + { url = "https://files.pythonhosted.org/packages/d6/53/6a06183544daba51c059123a2064a99039df25f115a06bdb26f2ea177038/pydantic_core-2.46.2-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2c2f6e32548ac8d559b47944effcf8ae4d81c161f6b6c885edc53bc08b8f192d", size = 2164816, upload-time = "2026-04-17T09:11:56.187Z" }, + { url = "https://files.pythonhosted.org/packages/57/6f/10fcdd9e3eca66fc828eef0f6f5850f2dd3bca2c59e6e041fb8bc3da39be/pydantic_core-2.46.2-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:b089a81c58e6ea0485562bbbbbca4f65c0549521606d5ef27fba217aac9b665a", size = 2166130, upload-time = "2026-04-17T09:10:03.804Z" }, + { url = "https://files.pythonhosted.org/packages/29/83/92d3fd0e0156cad2e3cb5c26de73794af78ac9fa0c22ab666e566dd67061/pydantic_core-2.46.2-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:7f700a6d6f64112ae9193709b84303bbab84424ad4b47d0253301aabce9dfc70", size = 2316605, upload-time = "2026-04-17T09:12:45.249Z" }, + { url = "https://files.pythonhosted.org/packages/97/f1/facffdb970981068219582e499b8d0871ed163ffcc6b347de5c412669e4c/pydantic_core-2.46.2-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:67db6814beaa5fefe91101ec7eb9efda613795767be96f7cf58b1ca8c9ca9972", size = 2358385, upload-time = "2026-04-17T09:09:54.657Z" }, + { url = "https://files.pythonhosted.org/packages/8b/a1/b8160b2f22b2199467bc68581a4ed380643c16b348a27d6165c6c242d694/pydantic_core-2.46.2-cp314-cp314t-win32.whl", hash = "sha256:32fbc7447be8e3be99bf7869f7066308f16be55b61f9882c2cefc7931f5c7664", size = 1942373, upload-time = "2026-04-17T09:12:59.594Z" }, + { url = "https://files.pythonhosted.org/packages/0d/90/db89acabe5b150e11d1b59fe3d947dda2ef6abbfef5c82f056ff63802f5d/pydantic_core-2.46.2-cp314-cp314t-win_amd64.whl", hash = "sha256:b317a2b97019c0b95ce99f4f901ae383f40132da6706cdf1731066a73394c25c", size = 2052078, upload-time = "2026-04-17T09:10:19.96Z" }, + { url = "https://files.pythonhosted.org/packages/97/32/e19b83ceb07a3f1bb21798407790bbc9a31740158fd132b94139cb84e16c/pydantic_core-2.46.2-cp314-cp314t-win_arm64.whl", hash = "sha256:7dcb9d40930dfad7ab6b20bcc6ca9d2b030b0f347a0cd9909b54bd53ead521b1", size = 2016941, upload-time = "2026-04-17T09:12:34.447Z" }, + { url = "https://files.pythonhosted.org/packages/25/ec/e91aa08df1c33d5e3c2b60c07a1eca9f21809728a824c7b467bb3bda68b5/pydantic_core-2.46.2-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:7c5a5b3dbb9e8918e223be6580da5ffcf861c0505bbc196ebed7176ce05b7b4e", size = 2105046, upload-time = "2026-04-17T09:10:55.614Z" }, + { url = "https://files.pythonhosted.org/packages/f0/73/27112400a0452e375290e7c40aef5cc9844ac0920fb1029238cfc68121fa/pydantic_core-2.46.2-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:bc1e8ce33d5a337f2ba862e0719b8201cd54aaed967406c748e009191d47efdd", size = 1940029, upload-time = "2026-04-17T09:12:21.5Z" }, + { url = "https://files.pythonhosted.org/packages/b1/44/3d39f782bc82ddd0b2d82bde83b408aa40a332cdf6f3018acb34e3d4dcfc/pydantic_core-2.46.2-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b737c0b280f41143266445de2689c0e49c79307e51c44ce3a77fef2bedad4994", size = 1987772, upload-time = "2026-04-17T09:10:02.357Z" }, + { url = "https://files.pythonhosted.org/packages/c4/1a/0242e5b7b6cf51dbccc065029f0420107b6bf7e191fcb918f5cb71218acf/pydantic_core-2.46.2-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1b877d597afb82b4898e35354bba55de6f7f048421ae0edadbb9886ec137b532", size = 2138468, upload-time = "2026-04-17T09:11:51.546Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d2/66c146f421178641bda880b0267c0d57dd84f5fec9ecc8e46be17b480742/pydantic_core-2.46.2-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:e9fcabd1857492b5bf16f90258babde50f618f55d046b1309972da2396321ff9", size = 2091621, upload-time = "2026-04-17T09:12:47.501Z" }, + { url = "https://files.pythonhosted.org/packages/ee/b2/c28419aa9fc8055f4ac8e801d1d11c6357351bfa4321ed9bafab3eb98087/pydantic_core-2.46.2-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:fb3ec2c7f54c07b30d89983ce78dc32c37dd06a972448b8716d609493802d628", size = 1937059, upload-time = "2026-04-17T09:10:53.554Z" }, + { url = "https://files.pythonhosted.org/packages/30/ce/cd0824a2db213dc17113291b7a09b9b0ccd9fbf97daa4b81548703341baf/pydantic_core-2.46.2-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:130a6c837d819ef33e8c2bf702ed2c3429237ea69807f1140943d6f4bdaf52fa", size = 1997278, upload-time = "2026-04-17T09:12:23.784Z" }, + { url = "https://files.pythonhosted.org/packages/c9/69/47283fe3c0c967d3e9e9cd6c42b70907610c8a6f8d6e8381f1bb55f8006c/pydantic_core-2.46.2-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c2e25417cec5cd9bddb151e33cb08c50160f317479ecc02b22a95ec18f8fe004", size = 2147096, upload-time = "2026-04-17T09:12:43.124Z" }, + { url = "https://files.pythonhosted.org/packages/16/d5/dec7c127fa722ff56e1ccf1e960ae1318a9f66742135e97bf9771447216f/pydantic_core-2.46.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c3ad79ed32004d9de91cacd4b5faaff44d56051392fe1d5526feda596f01af25", size = 2107613, upload-time = "2026-04-17T09:10:36.269Z" }, + { url = "https://files.pythonhosted.org/packages/bc/35/975c109b337260a71c93198baf663982b6b39fe3e584e279548a0969e5d4/pydantic_core-2.46.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d157c48d28eebe5d46906de06a6a2f2c9e00b67d3e42de1f1b9c2d42b810f77c", size = 1947099, upload-time = "2026-04-17T09:12:15.304Z" }, + { url = "https://files.pythonhosted.org/packages/4e/11/52a971a0f9218631690274be533f05e5ddde5547f0823bb3e9dfd1be49f6/pydantic_core-2.46.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b42c6471288dedc979ac8400d9c9770f03967dd187db1f8d3405d4d182cc714", size = 2133866, upload-time = "2026-04-17T09:12:27.994Z" }, + { url = "https://files.pythonhosted.org/packages/fe/7a/33d94d0698602b2d1712e78c703a33952eb2ca69e02e8e4b208e7f6602b5/pydantic_core-2.46.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4f27bc4801358dc070d6697b41237fce9923d8e69a1ce1e95606ac36c1552dc1", size = 2161721, upload-time = "2026-04-17T09:11:16.111Z" }, + { url = "https://files.pythonhosted.org/packages/b0/cb/0df7ee0a148e9ce0968a80787967ddca9f6b3f8a49152a881b88da262701/pydantic_core-2.46.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:e094a8f85db41aa7f6a45c5dac2950afc9862e66832934231962252b5d284eed", size = 2180175, upload-time = "2026-04-17T09:11:41.577Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a8/258a32878140347532be4e44c6f3b1ace3b52b9c9ca7548a65ce18adf4b4/pydantic_core-2.46.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:807eeda5551f6884d3b4421578be37be50ddb7a58832348e99617a6714a73748", size = 2319882, upload-time = "2026-04-17T09:10:21.872Z" }, + { url = "https://files.pythonhosted.org/packages/13/b9/5071c298a0f91314a5402b8c56e0efbcebe77085327d0b4df7dc9cb0b674/pydantic_core-2.46.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:fcaa1c3c846a7f6686b38fe493d1b2e8007380e293bfef6a9354563c026cbf36", size = 2348065, upload-time = "2026-04-17T09:11:08.263Z" }, + { url = "https://files.pythonhosted.org/packages/75/f3/0a7087e5f861d66ca64ce927230b397cc264c87b712156e6a93b26a459c8/pydantic_core-2.46.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:154dbfdfb11b8cbd8ff4d00d0b81e3d19f4cb4bedd5aa9f091060ba071474c6a", size = 2192159, upload-time = "2026-04-17T09:11:20.123Z" }, ] [[package]] name = "pygments" -version = "2.19.2" +version = "2.20.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, ] [[package]] name = "pyinfra" -version = "3.6.1" +version = "3.7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, @@ -1801,9 +1890,9 @@ dependencies = [ { name = "python-dateutil" }, { name = "typeguard" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/dc/36/7a5d4663dca48be0db267c7755a5de863a9f06fa238e066d0ae35c823ad5/pyinfra-3.6.1.tar.gz", hash = "sha256:03b56633fcd80226f454e47de6b0dfa42314ee88d6b2c1c5c31e30b009e9bd31", size = 569142, upload-time = "2026-02-02T13:47:09.375Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fa/2b/49eeb9000a66157f7cda8b5b4871e1e496f1fb42fd1b94290ee5efb9a3cb/pyinfra-3.7.tar.gz", hash = "sha256:2f8eeee84d4d90c5ee57300ae6673b58237117605cca6b0cffde29f0b2517d4b", size = 599054, upload-time = "2026-03-12T10:55:35.972Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fc/4a/204ec36f1f9ef924580e0dcef8968c53e5b43ba1c0d25a914b10f309ad6f/pyinfra-3.6.1-py3-none-any.whl", hash = "sha256:618c999cfc0db32b944077f76cb77f4696f01d3104e64d5c1719987f172582cc", size = 263493, upload-time = "2026-02-02T13:47:07.384Z" }, + { url = "https://files.pythonhosted.org/packages/a4/6d/246168fe781009d32199fb7553a9a462e298d17f6e49accedf3b16c65346/pyinfra-3.7-py3-none-any.whl", hash = "sha256:9208de3f4479ad80509db51da874445017987818dba8a36c808f56ce1451b34c", size = 267074, upload-time = "2026-03-12T10:55:34.518Z" }, ] [[package]] @@ -1843,7 +1932,7 @@ wheels = [ [[package]] name = "pytest" -version = "9.0.2" +version = "9.0.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -1852,9 +1941,9 @@ dependencies = [ { name = "pluggy" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, + { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, ] [[package]] @@ -1884,11 +1973,11 @@ wheels = [ [[package]] name = "python-json-logger" -version = "4.0.0" +version = "4.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/29/bf/eca6a3d43db1dae7070f70e160ab20b807627ba953663ba07928cdd3dc58/python_json_logger-4.0.0.tar.gz", hash = "sha256:f58e68eb46e1faed27e0f574a55a0455eecd7b8a5b88b85a784519ba3cff047f", size = 17683, upload-time = "2025-10-06T04:15:18.984Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f7/ff/3cc9165fd44106973cd7ac9facb674a65ed853494592541d339bdc9a30eb/python_json_logger-4.1.0.tar.gz", hash = "sha256:b396b9e3ed782b09ff9d6e4f1683d46c83ad0d35d2e407c09a9ebbf038f88195", size = 17573, upload-time = "2026-03-29T04:39:56.805Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/51/e5/fecf13f06e5e5f67e8837d777d1bc43fac0ed2b77a676804df5c34744727/python_json_logger-4.0.0-py3-none-any.whl", hash = "sha256:af09c9daf6a813aa4cc7180395f50f2a9e5fa056034c9953aec92e381c5ba1e2", size = 15548, upload-time = "2025-10-06T04:15:17.553Z" }, + { url = "https://files.pythonhosted.org/packages/27/be/0631a861af4d1c875f096c07d34e9a63639560a717130e7a87cbc82b7e3f/python_json_logger-4.1.0-py3-none-any.whl", hash = "sha256:132994765cf75bf44554be9aa49b06ef2345d23661a96720262716438141b6b2", size = 15021, upload-time = "2026-03-29T04:39:55.266Z" }, ] [[package]] @@ -2049,7 +2138,7 @@ wheels = [ [[package]] name = "requests" -version = "2.32.5" +version = "2.33.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, @@ -2057,9 +2146,9 @@ dependencies = [ { name = "idna" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/a4/98b9c7c6428a668bf7e42ebb7c79d576a1c3c1e3ae2d47e674b468388871/requests-2.33.1.tar.gz", hash = "sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517", size = 134120, upload-time = "2026-03-30T16:09:15.531Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, + { url = "https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a", size = 64947, upload-time = "2026-03-30T16:09:13.83Z" }, ] [[package]] @@ -2097,15 +2186,15 @@ wheels = [ [[package]] name = "rich" -version = "14.3.3" +version = "15.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown-it-py" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b3/c6/f3b320c27991c46f43ee9d856302c70dc2d0fb2dba4842ff739d5f46b393/rich-14.3.3.tar.gz", hash = "sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b", size = 230582, upload-time = "2026-02-19T17:23:12.474Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl", hash = "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d", size = 310458, upload-time = "2026-02-19T17:23:13.732Z" }, + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, ] [[package]] @@ -2218,27 +2307,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.15.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/da/31/d6e536cdebb6568ae75a7f00e4b4819ae0ad2640c3604c305a0428680b0c/ruff-0.15.4.tar.gz", hash = "sha256:3412195319e42d634470cc97aa9803d07e9d5c9223b99bcb1518f0c725f26ae1", size = 4569550, upload-time = "2026-02-26T20:04:14.959Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f2/82/c11a03cfec3a4d26a0ea1e571f0f44be5993b923f905eeddfc397c13d360/ruff-0.15.4-py3-none-linux_armv6l.whl", hash = "sha256:a1810931c41606c686bae8b5b9a8072adac2f611bb433c0ba476acba17a332e0", size = 10453333, upload-time = "2026-02-26T20:04:20.093Z" }, - { url = "https://files.pythonhosted.org/packages/ce/5d/6a1f271f6e31dffb31855996493641edc3eef8077b883eaf007a2f1c2976/ruff-0.15.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:5a1632c66672b8b4d3e1d1782859e98d6e0b4e70829530666644286600a33992", size = 10853356, upload-time = "2026-02-26T20:04:05.808Z" }, - { url = "https://files.pythonhosted.org/packages/b1/d8/0fab9f8842b83b1a9c2bf81b85063f65e93fb512e60effa95b0be49bfc54/ruff-0.15.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a4386ba2cd6c0f4ff75252845906acc7c7c8e1ac567b7bc3d373686ac8c222ba", size = 10187434, upload-time = "2026-02-26T20:03:54.656Z" }, - { url = "https://files.pythonhosted.org/packages/85/cc/cc220fd9394eff5db8d94dec199eec56dd6c9f3651d8869d024867a91030/ruff-0.15.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2496488bdfd3732747558b6f95ae427ff066d1fcd054daf75f5a50674411e75", size = 10535456, upload-time = "2026-02-26T20:03:52.738Z" }, - { url = "https://files.pythonhosted.org/packages/fa/0f/bced38fa5cf24373ec767713c8e4cadc90247f3863605fb030e597878661/ruff-0.15.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3f1c4893841ff2d54cbda1b2860fa3260173df5ddd7b95d370186f8a5e66a4ac", size = 10287772, upload-time = "2026-02-26T20:04:08.138Z" }, - { url = "https://files.pythonhosted.org/packages/2b/90/58a1802d84fed15f8f281925b21ab3cecd813bde52a8ca033a4de8ab0e7a/ruff-0.15.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:820b8766bd65503b6c30aaa6331e8ef3a6e564f7999c844e9a547c40179e440a", size = 11049051, upload-time = "2026-02-26T20:04:03.53Z" }, - { url = "https://files.pythonhosted.org/packages/d2/ac/b7ad36703c35f3866584564dc15f12f91cb1a26a897dc2fd13d7cb3ae1af/ruff-0.15.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c9fb74bab47139c1751f900f857fa503987253c3ef89129b24ed375e72873e85", size = 11890494, upload-time = "2026-02-26T20:04:10.497Z" }, - { url = "https://files.pythonhosted.org/packages/93/3d/3eb2f47a39a8b0da99faf9c54d3eb24720add1e886a5309d4d1be73a6380/ruff-0.15.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f80c98765949c518142b3a50a5db89343aa90f2c2bf7799de9986498ae6176db", size = 11326221, upload-time = "2026-02-26T20:04:12.84Z" }, - { url = "https://files.pythonhosted.org/packages/ff/90/bf134f4c1e5243e62690e09d63c55df948a74084c8ac3e48a88468314da6/ruff-0.15.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:451a2e224151729b3b6c9ffb36aed9091b2996fe4bdbd11f47e27d8f2e8888ec", size = 11168459, upload-time = "2026-02-26T20:04:00.969Z" }, - { url = "https://files.pythonhosted.org/packages/b5/e5/a64d27688789b06b5d55162aafc32059bb8c989c61a5139a36e1368285eb/ruff-0.15.4-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a8f157f2e583c513c4f5f896163a93198297371f34c04220daf40d133fdd4f7f", size = 11104366, upload-time = "2026-02-26T20:03:48.099Z" }, - { url = "https://files.pythonhosted.org/packages/f1/f6/32d1dcb66a2559763fc3027bdd65836cad9eb09d90f2ed6a63d8e9252b02/ruff-0.15.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:917cc68503357021f541e69b35361c99387cdbbf99bd0ea4aa6f28ca99ff5338", size = 10510887, upload-time = "2026-02-26T20:03:45.771Z" }, - { url = "https://files.pythonhosted.org/packages/ff/92/22d1ced50971c5b6433aed166fcef8c9343f567a94cf2b9d9089f6aa80fe/ruff-0.15.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e9737c8161da79fd7cfec19f1e35620375bd8b2a50c3e77fa3d2c16f574105cc", size = 10285939, upload-time = "2026-02-26T20:04:22.42Z" }, - { url = "https://files.pythonhosted.org/packages/e6/f4/7c20aec3143837641a02509a4668fb146a642fd1211846634edc17eb5563/ruff-0.15.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:291258c917539e18f6ba40482fe31d6f5ac023994ee11d7bdafd716f2aab8a68", size = 10765471, upload-time = "2026-02-26T20:03:58.924Z" }, - { url = "https://files.pythonhosted.org/packages/d0/09/6d2f7586f09a16120aebdff8f64d962d7c4348313c77ebb29c566cefc357/ruff-0.15.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:3f83c45911da6f2cd5936c436cf86b9f09f09165f033a99dcf7477e34041cbc3", size = 11263382, upload-time = "2026-02-26T20:04:24.424Z" }, - { url = "https://files.pythonhosted.org/packages/1b/fa/2ef715a1cd329ef47c1a050e10dee91a9054b7ce2fcfdd6a06d139afb7ec/ruff-0.15.4-py3-none-win32.whl", hash = "sha256:65594a2d557d4ee9f02834fcdf0a28daa8b3b9f6cb2cb93846025a36db47ef22", size = 10506664, upload-time = "2026-02-26T20:03:50.56Z" }, - { url = "https://files.pythonhosted.org/packages/d0/a8/c688ef7e29983976820d18710f955751d9f4d4eb69df658af3d006e2ba3e/ruff-0.15.4-py3-none-win_amd64.whl", hash = "sha256:04196ad44f0df220c2ece5b0e959c2f37c777375ec744397d21d15b50a75264f", size = 11651048, upload-time = "2026-02-26T20:04:17.191Z" }, - { url = "https://files.pythonhosted.org/packages/3e/0a/9e1be9035b37448ce2e68c978f0591da94389ade5a5abafa4cf99985d1b2/ruff-0.15.4-py3-none-win_arm64.whl", hash = "sha256:60d5177e8cfc70e51b9c5fad936c634872a74209f934c1e79107d11787ad5453", size = 10966776, upload-time = "2026-02-26T20:03:56.908Z" }, +version = "0.15.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/8d/192f3d7103816158dfd5ea50d098ef2aec19194e6cbccd4b3485bdb2eb2d/ruff-0.15.11.tar.gz", hash = "sha256:f092b21708bf0e7437ce9ada249dfe688ff9a0954fc94abab05dcea7dcd29c33", size = 4637264, upload-time = "2026-04-16T18:46:26.58Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/1e/6aca3427f751295ab011828e15e9bf452200ac74484f1db4be0197b8170b/ruff-0.15.11-py3-none-linux_armv6l.whl", hash = "sha256:e927cfff503135c558eb581a0c9792264aae9507904eb27809cdcff2f2c847b7", size = 10607943, upload-time = "2026-04-16T18:46:05.967Z" }, + { url = "https://files.pythonhosted.org/packages/e7/26/1341c262e74f36d4e84f3d6f4df0ac68cd53331a66bfc5080daa17c84c0b/ruff-0.15.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7a1b5b2938d8f890b76084d4fa843604d787a912541eae85fd7e233398bbb73e", size = 10988592, upload-time = "2026-04-16T18:46:00.742Z" }, + { url = "https://files.pythonhosted.org/packages/03/71/850b1d6ffa9564fbb6740429bad53df1094082fe515c8c1e74b6d8d05f18/ruff-0.15.11-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d4176f3d194afbdaee6e41b9ccb1a2c287dba8700047df474abfbe773825d1cb", size = 10338501, upload-time = "2026-04-16T18:46:03.723Z" }, + { url = "https://files.pythonhosted.org/packages/f2/11/cc1284d3e298c45a817a6aadb6c3e1d70b45c9b36d8d9cce3387b495a03a/ruff-0.15.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b17c886fb88203ced3afe7f14e8d5ae96e9d2f4ccc0ee66aa19f2c2675a27e4", size = 10670693, upload-time = "2026-04-16T18:46:41.941Z" }, + { url = "https://files.pythonhosted.org/packages/ce/9e/f8288b034ab72b371513c13f9a41d9ba3effac54e24bfb467b007daee2ca/ruff-0.15.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:49fafa220220afe7758a487b048de4c8f9f767f37dfefad46b9dd06759d003eb", size = 10416177, upload-time = "2026-04-16T18:46:21.717Z" }, + { url = "https://files.pythonhosted.org/packages/85/71/504d79abfd3d92532ba6bbe3d1c19fada03e494332a59e37c7c2dabae427/ruff-0.15.11-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2ab8427e74a00d93b8bda1307b1e60970d40f304af38bccb218e056c220120d", size = 11221886, upload-time = "2026-04-16T18:46:15.086Z" }, + { url = "https://files.pythonhosted.org/packages/43/5a/947e6ab7a5ad603d65b474be15a4cbc6d29832db5d762cd142e4e3a74164/ruff-0.15.11-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:195072c0c8e1fc8f940652073df082e37a5d9cb43b4ab1e4d0566ab8977a13b7", size = 12075183, upload-time = "2026-04-16T18:46:07.944Z" }, + { url = "https://files.pythonhosted.org/packages/9f/a1/0b7bb6268775fdd3a0818aee8efd8f5b4e231d24dd4d528ced2534023182/ruff-0.15.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a3a0996d486af3920dec930a2e7daed4847dfc12649b537a9335585ada163e9e", size = 11516575, upload-time = "2026-04-16T18:46:31.687Z" }, + { url = "https://files.pythonhosted.org/packages/30/c3/bb5168fc4d233cc06e95f482770d0f3c87945a0cd9f614b90ea8dc2f2833/ruff-0.15.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bef2cb556d509259f1fe440bb9cd33c756222cf0a7afe90d15edf0866702431", size = 11306537, upload-time = "2026-04-16T18:46:36.988Z" }, + { url = "https://files.pythonhosted.org/packages/e4/92/4cfae6441f3967317946f3b788136eecf093729b94d6561f963ed810c82e/ruff-0.15.11-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:030d921a836d7d4a12cf6e8d984a88b66094ccb0e0f17ddd55067c331191bf19", size = 11296813, upload-time = "2026-04-16T18:46:24.182Z" }, + { url = "https://files.pythonhosted.org/packages/43/26/972784c5dde8313acde8ac71ba8ac65475b85db4a2352a76c9934361f9bc/ruff-0.15.11-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:0e783b599b4577788dbbb66b9addcef87e9a8832f4ce0c19e34bf55543a2f890", size = 10633136, upload-time = "2026-04-16T18:46:39.802Z" }, + { url = "https://files.pythonhosted.org/packages/5b/53/3985a4f185020c2f367f2e08a103032e12564829742a1b417980ce1514a0/ruff-0.15.11-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ae90592246625ba4a34349d68ec28d4400d75182b71baa196ddb9f82db025ef5", size = 10424701, upload-time = "2026-04-16T18:46:10.381Z" }, + { url = "https://files.pythonhosted.org/packages/d3/57/bf0dfb32241b56c83bb663a826133da4bf17f682ba8c096973065f6e6a68/ruff-0.15.11-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1f111d62e3c983ed20e0ca2e800f8d77433a5b1161947df99a5c2a3fb60514f0", size = 10873887, upload-time = "2026-04-16T18:46:29.157Z" }, + { url = "https://files.pythonhosted.org/packages/02/05/e48076b2a57dc33ee8c7a957296f97c744ca891a8ffb4ffb1aaa3b3f517d/ruff-0.15.11-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:06f483d6646f59eaffba9ae30956370d3a886625f511a3108994000480621d1c", size = 11404316, upload-time = "2026-04-16T18:46:19.462Z" }, + { url = "https://files.pythonhosted.org/packages/88/27/0195d15fe7a897cbcba0904792c4b7c9fdd958456c3a17d2ea6093716a9a/ruff-0.15.11-py3-none-win32.whl", hash = "sha256:476a2aa56b7da0b73a3ee80b6b2f0e19cce544245479adde7baa65466664d5f3", size = 10655535, upload-time = "2026-04-16T18:46:12.47Z" }, + { url = "https://files.pythonhosted.org/packages/3a/5e/c927b325bd4c1d3620211a4b96f47864633199feed60fa936025ab27e090/ruff-0.15.11-py3-none-win_amd64.whl", hash = "sha256:8b6756d88d7e234fb0c98c91511aae3cd519d5e3ed271cae31b20f39cb2a12a3", size = 11779692, upload-time = "2026-04-16T18:46:17.268Z" }, + { url = "https://files.pythonhosted.org/packages/63/b6/aeadee5443e49baa2facd51131159fd6301cc4ccfc1541e4df7b021c37dd/ruff-0.15.11-py3-none-win_arm64.whl", hash = "sha256:063fed18cc1bbe0ee7393957284a6fe8b588c6a406a285af3ee3f46da2391ee4", size = 11032614, upload-time = "2026-04-16T18:46:34.487Z" }, ] [[package]] @@ -2252,11 +2341,11 @@ wheels = [ [[package]] name = "setuptools" -version = "82.0.0" +version = "82.0.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/82/f3/748f4d6f65d1756b9ae577f329c951cda23fb900e4de9f70900ced962085/setuptools-82.0.0.tar.gz", hash = "sha256:22e0a2d69474c6ae4feb01951cb69d515ed23728cf96d05513d36e42b62b37cb", size = 1144893, upload-time = "2026-02-08T15:08:40.206Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4f/db/cfac1baf10650ab4d1c111714410d2fbb77ac5a616db26775db562c8fab2/setuptools-82.0.1.tar.gz", hash = "sha256:7d872682c5d01cfde07da7bccc7b65469d3dca203318515ada1de5eda35efbf9", size = 1152316, upload-time = "2026-03-09T12:47:17.221Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e1/c6/76dc613121b793286a3f91621d7b75a2b493e0390ddca50f11993eadf192/setuptools-82.0.0-py3-none-any.whl", hash = "sha256:70b18734b607bd1da571d097d236cfcfacaf01de45717d59e6e04b96877532e0", size = 1003468, upload-time = "2026-02-08T15:08:38.723Z" }, + { url = "https://files.pythonhosted.org/packages/9d/76/f789f7a86709c6b087c5a2f52f911838cad707cc613162401badc665acfe/setuptools-82.0.1-py3-none-any.whl", hash = "sha256:a59e362652f08dcd477c78bb6e7bd9d80a7995bc73ce773050228a348ce2e5bb", size = 1006223, upload-time = "2026-03-09T12:47:15.026Z" }, ] [[package]] @@ -2337,21 +2426,19 @@ wheels = [ [[package]] name = "tornado" -version = "6.5.4" +version = "6.5.5" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/37/1d/0a336abf618272d53f62ebe274f712e213f5a03c0b2339575430b8362ef2/tornado-6.5.4.tar.gz", hash = "sha256:a22fa9047405d03260b483980635f0b041989d8bcc9a313f8fe18b411d84b1d7", size = 513632, upload-time = "2025-12-15T19:21:03.836Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/f1/3173dfa4a18db4a9b03e5d55325559dab51ee653763bb8745a75af491286/tornado-6.5.5.tar.gz", hash = "sha256:192b8f3ea91bd7f1f50c06955416ed76c6b72f96779b962f07f911b91e8d30e9", size = 516006, upload-time = "2026-03-10T21:31:02.067Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ab/a9/e94a9d5224107d7ce3cc1fab8d5dc97f5ea351ccc6322ee4fb661da94e35/tornado-6.5.4-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:d6241c1a16b1c9e4cc28148b1cda97dd1c6cb4fb7068ac1bedc610768dff0ba9", size = 443909, upload-time = "2025-12-15T19:20:48.382Z" }, - { url = "https://files.pythonhosted.org/packages/db/7e/f7b8d8c4453f305a51f80dbb49014257bb7d28ccb4bbb8dd328ea995ecad/tornado-6.5.4-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2d50f63dda1d2cac3ae1fa23d254e16b5e38153758470e9956cbc3d813d40843", size = 442163, upload-time = "2025-12-15T19:20:49.791Z" }, - { url = "https://files.pythonhosted.org/packages/ba/b5/206f82d51e1bfa940ba366a8d2f83904b15942c45a78dd978b599870ab44/tornado-6.5.4-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1cf66105dc6acb5af613c054955b8137e34a03698aa53272dbda4afe252be17", size = 445746, upload-time = "2025-12-15T19:20:51.491Z" }, - { url = "https://files.pythonhosted.org/packages/8e/9d/1a3338e0bd30ada6ad4356c13a0a6c35fbc859063fa7eddb309183364ac1/tornado-6.5.4-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50ff0a58b0dc97939d29da29cd624da010e7f804746621c78d14b80238669335", size = 445083, upload-time = "2025-12-15T19:20:52.778Z" }, - { url = "https://files.pythonhosted.org/packages/50/d4/e51d52047e7eb9a582da59f32125d17c0482d065afd5d3bc435ff2120dc5/tornado-6.5.4-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5fb5e04efa54cf0baabdd10061eb4148e0be137166146fff835745f59ab9f7f", size = 445315, upload-time = "2025-12-15T19:20:53.996Z" }, - { url = "https://files.pythonhosted.org/packages/27/07/2273972f69ca63dbc139694a3fc4684edec3ea3f9efabf77ed32483b875c/tornado-6.5.4-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9c86b1643b33a4cd415f8d0fe53045f913bf07b4a3ef646b735a6a86047dda84", size = 446003, upload-time = "2025-12-15T19:20:56.101Z" }, - { url = "https://files.pythonhosted.org/packages/d1/83/41c52e47502bf7260044413b6770d1a48dda2f0246f95ee1384a3cd9c44a/tornado-6.5.4-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:6eb82872335a53dd063a4f10917b3efd28270b56a33db69009606a0312660a6f", size = 445412, upload-time = "2025-12-15T19:20:57.398Z" }, - { url = "https://files.pythonhosted.org/packages/10/c7/bc96917f06cbee182d44735d4ecde9c432e25b84f4c2086143013e7b9e52/tornado-6.5.4-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6076d5dda368c9328ff41ab5d9dd3608e695e8225d1cd0fd1e006f05da3635a8", size = 445392, upload-time = "2025-12-15T19:20:58.692Z" }, - { url = "https://files.pythonhosted.org/packages/0c/1a/d7592328d037d36f2d2462f4bc1fbb383eec9278bc786c1b111cbbd44cfa/tornado-6.5.4-cp39-abi3-win32.whl", hash = "sha256:1768110f2411d5cd281bac0a090f707223ce77fd110424361092859e089b38d1", size = 446481, upload-time = "2025-12-15T19:21:00.008Z" }, - { url = "https://files.pythonhosted.org/packages/d6/6d/c69be695a0a64fd37a97db12355a035a6d90f79067a3cf936ec2b1dc38cd/tornado-6.5.4-cp39-abi3-win_amd64.whl", hash = "sha256:fa07d31e0cd85c60713f2b995da613588aa03e1303d75705dca6af8babc18ddc", size = 446886, upload-time = "2025-12-15T19:21:01.287Z" }, - { url = "https://files.pythonhosted.org/packages/50/49/8dc3fd90902f70084bd2cd059d576ddb4f8bb44c2c7c0e33a11422acb17e/tornado-6.5.4-cp39-abi3-win_arm64.whl", hash = "sha256:053e6e16701eb6cbe641f308f4c1a9541f91b6261991160391bfc342e8a551a1", size = 445910, upload-time = "2025-12-15T19:21:02.571Z" }, + { url = "https://files.pythonhosted.org/packages/59/8c/77f5097695f4dd8255ecbd08b2a1ed8ba8b953d337804dd7080f199e12bf/tornado-6.5.5-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:487dc9cc380e29f58c7ab88f9e27cdeef04b2140862e5076a66fb6bb68bb1bfa", size = 445983, upload-time = "2026-03-10T21:30:44.28Z" }, + { url = "https://files.pythonhosted.org/packages/ab/5e/7625b76cd10f98f1516c36ce0346de62061156352353ef2da44e5c21523c/tornado-6.5.5-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:65a7f1d46d4bb41df1ac99f5fcb685fb25c7e61613742d5108b010975a9a6521", size = 444246, upload-time = "2026-03-10T21:30:46.571Z" }, + { url = "https://files.pythonhosted.org/packages/b2/04/7b5705d5b3c0fab088f434f9c83edac1573830ca49ccf29fb83bf7178eec/tornado-6.5.5-cp39-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e74c92e8e65086b338fd56333fb9a68b9f6f2fe7ad532645a290a464bcf46be5", size = 447229, upload-time = "2026-03-10T21:30:48.273Z" }, + { url = "https://files.pythonhosted.org/packages/34/01/74e034a30ef59afb4097ef8659515e96a39d910b712a89af76f5e4e1f93c/tornado-6.5.5-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:435319e9e340276428bbdb4e7fa732c2d399386d1de5686cb331ec8eee754f07", size = 448192, upload-time = "2026-03-10T21:30:51.22Z" }, + { url = "https://files.pythonhosted.org/packages/be/00/fe9e02c5a96429fce1a1d15a517f5d8444f9c412e0bb9eadfbe3b0fc55bf/tornado-6.5.5-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:3f54aa540bdbfee7b9eb268ead60e7d199de5021facd276819c193c0fb28ea4e", size = 448039, upload-time = "2026-03-10T21:30:53.52Z" }, + { url = "https://files.pythonhosted.org/packages/82/9e/656ee4cec0398b1d18d0f1eb6372c41c6b889722641d84948351ae19556d/tornado-6.5.5-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:36abed1754faeb80fbd6e64db2758091e1320f6bba74a4cf8c09cd18ccce8aca", size = 447445, upload-time = "2026-03-10T21:30:55.541Z" }, + { url = "https://files.pythonhosted.org/packages/5a/76/4921c00511f88af86a33de770d64141170f1cfd9c00311aea689949e274e/tornado-6.5.5-cp39-abi3-win32.whl", hash = "sha256:dd3eafaaeec1c7f2f8fdcd5f964e8907ad788fe8a5a32c4426fbbdda621223b7", size = 448582, upload-time = "2026-03-10T21:30:57.142Z" }, + { url = "https://files.pythonhosted.org/packages/2c/23/f6c6112a04d28eed765e374435fb1a9198f73e1ec4b4024184f21faeb1ad/tornado-6.5.5-cp39-abi3-win_amd64.whl", hash = "sha256:6443a794ba961a9f619b1ae926a2e900ac20c34483eea67be4ed8f1e58d3ef7b", size = 448990, upload-time = "2026-03-10T21:30:58.857Z" }, + { url = "https://files.pythonhosted.org/packages/b7/c8/876602cbc96469911f0939f703453c1157b0c826ecb05bdd32e023397d4e/tornado-6.5.5-cp39-abi3-win_arm64.whl", hash = "sha256:2c9a876e094109333f888539ddb2de4361743e5d21eece20688e3e351e4990a6", size = 448016, upload-time = "2026-03-10T21:31:00.43Z" }, ] [[package]] @@ -2413,11 +2500,11 @@ wheels = [ [[package]] name = "tzdata" -version = "2025.3" +version = "2026.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7", size = 196772, upload-time = "2025-12-13T17:45:35.667Z" } +sdist = { url = "https://files.pythonhosted.org/packages/19/f5/cd531b2d15a671a40c0f66cf06bc3570a12cd56eef98960068ebbad1bf5a/tzdata-2026.1.tar.gz", hash = "sha256:67658a1903c75917309e753fdc349ac0efd8c27db7a0cb406a25be4840f87f98", size = 197639, upload-time = "2026-04-03T11:25:22.002Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" }, + { url = "https://files.pythonhosted.org/packages/b0/70/d460bd685a170790ec89317e9bd33047988e4bce507b831f5db771e142de/tzdata-2026.1-py2.py3-none-any.whl", hash = "sha256:4b1d2be7ac37ceafd7327b961aa3a54e467efbdb563a23655fbfe0d39cfc42a9", size = 348952, upload-time = "2026-04-03T11:25:20.313Z" }, ] [[package]] @@ -2647,32 +2734,32 @@ wheels = [ [[package]] name = "zope-interface" -version = "8.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/86/a4/77daa5ba398996d16bb43fc721599d27d03eae68fe3c799de1963c72e228/zope_interface-8.2.tar.gz", hash = "sha256:afb20c371a601d261b4f6edb53c3c418c249db1a9717b0baafc9a9bb39ba1224", size = 254019, upload-time = "2026-01-09T07:51:07.253Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/98/97/9c2aa8caae79915ed64eb114e18816f178984c917aa9adf2a18345e4f2e5/zope_interface-8.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c65ade7ea85516e428651048489f5e689e695c79188761de8c622594d1e13322", size = 208081, upload-time = "2026-01-09T08:05:06.623Z" }, - { url = "https://files.pythonhosted.org/packages/34/86/4e2fcb01a8f6780ac84923748e450af0805531f47c0956b83065c99ab543/zope_interface-8.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a1ef4b43659e1348f35f38e7d1a6bbc1682efde239761f335ffc7e31e798b65b", size = 208522, upload-time = "2026-01-09T08:05:07.986Z" }, - { url = "https://files.pythonhosted.org/packages/f6/eb/08e277da32ddcd4014922854096cf6dcb7081fad415892c2da1bedefbf02/zope_interface-8.2-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:dfc4f44e8de2ff4eba20af4f0a3ca42d3c43ab24a08e49ccd8558b7a4185b466", size = 255198, upload-time = "2026-01-09T08:05:09.532Z" }, - { url = "https://files.pythonhosted.org/packages/ea/a1/b32484f3281a5dc83bc713ad61eca52c543735cdf204543172087a074a74/zope_interface-8.2-cp311-cp311-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8f094bfb49179ec5dc9981cb769af1275702bd64720ef94874d9e34da1390d4c", size = 259970, upload-time = "2026-01-09T08:05:11.477Z" }, - { url = "https://files.pythonhosted.org/packages/f6/81/bca0e8ae1e487d4093a8a7cfed2118aa2d4758c8cfd66e59d2af09d71f1c/zope_interface-8.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d2bb8e7364e18f083bf6744ccf30433b2a5f236c39c95df8514e3c13007098ce", size = 261153, upload-time = "2026-01-09T08:05:13.402Z" }, - { url = "https://files.pythonhosted.org/packages/40/1e/e3ff2a708011e56b10b271b038d4cb650a8ad5b7d24352fe2edf6d6b187a/zope_interface-8.2-cp311-cp311-win_amd64.whl", hash = "sha256:6f4b4dfcfdfaa9177a600bb31cebf711fdb8c8e9ed84f14c61c420c6aa398489", size = 212330, upload-time = "2026-01-09T08:05:15.267Z" }, - { url = "https://files.pythonhosted.org/packages/e0/a0/1e1fabbd2e9c53ef92b69df6d14f4adc94ec25583b1380336905dc37e9a0/zope_interface-8.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:624b6787fc7c3e45fa401984f6add2c736b70a7506518c3b537ffaacc4b29d4c", size = 208785, upload-time = "2026-01-09T08:05:17.348Z" }, - { url = "https://files.pythonhosted.org/packages/c3/2a/88d098a06975c722a192ef1fb7d623d1b57c6a6997cf01a7aabb45ab1970/zope_interface-8.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bc9ded9e97a0ed17731d479596ed1071e53b18e6fdb2fc33af1e43f5fd2d3aaa", size = 208976, upload-time = "2026-01-09T08:05:18.792Z" }, - { url = "https://files.pythonhosted.org/packages/e9/e8/757398549fdfd2f8c89f32c82ae4d2f0537ae2a5d2f21f4a2f711f5a059f/zope_interface-8.2-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:532367553e4420c80c0fc0cabcc2c74080d495573706f66723edee6eae53361d", size = 259411, upload-time = "2026-01-09T08:05:20.567Z" }, - { url = "https://files.pythonhosted.org/packages/91/af/502601f0395ce84dff622f63cab47488657a04d0065547df42bee3a680ff/zope_interface-8.2-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2bf9cf275468bafa3c72688aad8cfcbe3d28ee792baf0b228a1b2d93bd1d541a", size = 264859, upload-time = "2026-01-09T08:05:22.234Z" }, - { url = "https://files.pythonhosted.org/packages/89/0c/d2f765b9b4814a368a7c1b0ac23b68823c6789a732112668072fe596945d/zope_interface-8.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0009d2d3c02ea783045d7804da4fd016245e5c5de31a86cebba66dd6914d59a2", size = 264398, upload-time = "2026-01-09T08:05:23.853Z" }, - { url = "https://files.pythonhosted.org/packages/4a/81/2f171fbc4222066957e6b9220c4fb9146792540102c37e6d94e5d14aad97/zope_interface-8.2-cp312-cp312-win_amd64.whl", hash = "sha256:845d14e580220ae4544bd4d7eb800f0b6034fe5585fc2536806e0a26c2ee6640", size = 212444, upload-time = "2026-01-09T08:05:25.148Z" }, - { url = "https://files.pythonhosted.org/packages/66/47/45188fb101fa060b20e6090e500682398ab415e516a0c228fbb22bc7def2/zope_interface-8.2-cp313-cp313-macosx_10_9_x86_64.whl", hash = "sha256:6068322004a0158c80dfd4708dfb103a899635408c67c3b10e9acec4dbacefec", size = 209170, upload-time = "2026-01-09T08:05:26.616Z" }, - { url = "https://files.pythonhosted.org/packages/09/03/f6b9336c03c2b48403c4eb73a1ec961d94dc2fb5354c583dfb5fa05fd41f/zope_interface-8.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2499de92e8275d0dd68f84425b3e19e9268cd1fa8507997900fa4175f157733c", size = 209229, upload-time = "2026-01-09T08:05:28.521Z" }, - { url = "https://files.pythonhosted.org/packages/07/b1/65fe1dca708569f302ade02e6cdca309eab6752bc9f80105514f5b708651/zope_interface-8.2-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:f777e68c76208503609c83ca021a6864902b646530a1a39abb9ed310d1100664", size = 259393, upload-time = "2026-01-09T08:05:29.897Z" }, - { url = "https://files.pythonhosted.org/packages/eb/a5/97b49cfceb6ed53d3dcfb3f3ebf24d83b5553194f0337fbbb3a9fec6cf78/zope_interface-8.2-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9b05a919fdb0ed6ea942e5a7800e09a8b6cdae6f98fee1bef1c9d1a3fc43aaa0", size = 264863, upload-time = "2026-01-09T08:05:31.501Z" }, - { url = "https://files.pythonhosted.org/packages/cb/02/0b7a77292810efe3a0586a505b077ebafd5114e10c6e6e659f0c8e387e1f/zope_interface-8.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ccc62b5712dd7bd64cfba3ee63089fb11e840f5914b990033beeae3b2180b6cb", size = 264369, upload-time = "2026-01-09T08:05:32.941Z" }, - { url = "https://files.pythonhosted.org/packages/fb/1d/0d1ff3846302ed1b5bbf659316d8084b30106770a5f346b7ff4e9f540f80/zope_interface-8.2-cp313-cp313-win_amd64.whl", hash = "sha256:34f877d1d3bb7565c494ed93828fa6417641ca26faf6e8f044e0d0d500807028", size = 212447, upload-time = "2026-01-09T08:05:35.064Z" }, - { url = "https://files.pythonhosted.org/packages/1a/da/3c89de3917751446728b8898b4d53318bc2f8f6bf8196e150a063c59905e/zope_interface-8.2-cp314-cp314-macosx_10_9_x86_64.whl", hash = "sha256:46c7e4e8cbc698398a67e56ca985d19cb92365b4aafbeb6a712e8c101090f4cb", size = 209223, upload-time = "2026-01-09T08:05:36.449Z" }, - { url = "https://files.pythonhosted.org/packages/00/7f/62d00ec53f0a6e5df0c984781e6f3999ed265129c4c3413df8128d1e0207/zope_interface-8.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a87fc7517f825a97ff4a4ca4c8a950593c59e0f8e7bfe1b6f898a38d5ba9f9cf", size = 209366, upload-time = "2026-01-09T08:05:38.197Z" }, - { url = "https://files.pythonhosted.org/packages/ef/a2/f241986315174be8e00aabecfc2153cf8029c1327cab8ed53a9d979d7e08/zope_interface-8.2-cp314-cp314-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:ccf52f7d44d669203c2096c1a0c2c15d52e36b2e7a9413df50f48392c7d4d080", size = 261037, upload-time = "2026-01-09T08:05:39.568Z" }, - { url = "https://files.pythonhosted.org/packages/02/cc/b321c51d6936ede296a1b8860cf173bee2928357fe1fff7f97234899173f/zope_interface-8.2-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:aae807efc7bd26302eb2fea05cd6de7d59269ed6ae23a6de1ee47add6de99b8c", size = 264219, upload-time = "2026-01-09T08:05:41.624Z" }, - { url = "https://files.pythonhosted.org/packages/ab/fb/5f5e7b40a2f4efd873fe173624795ca47eaa22e29051270c981361b45209/zope_interface-8.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:05a0e42d6d830f547e114de2e7cd15750dc6c0c78f8138e6c5035e51ddfff37c", size = 264390, upload-time = "2026-01-09T08:05:42.936Z" }, - { url = "https://files.pythonhosted.org/packages/f9/82/3f2bc594370bc3abd58e5f9085d263bf682a222f059ed46275cde0570810/zope_interface-8.2-cp314-cp314-win_amd64.whl", hash = "sha256:561ce42390bee90bae51cf1c012902a8033b2aaefbd0deed81e877562a116d48", size = 212585, upload-time = "2026-01-09T08:05:44.419Z" }, +version = "8.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/04/0b1d92e7d31507c5fbe203d9cc1ae80fb0645688c7af751ea0ec18c2223e/zope_interface-8.3.tar.gz", hash = "sha256:e1a9de7d0b5b5c249a73b91aebf4598ce05e334303af6aa94865893283e9ff10", size = 256822, upload-time = "2026-04-10T06:12:35.036Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/99/cee01c7e8be6c5889f2c74914196decd91170011f420c9912792336f284c/zope_interface-8.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e8964f1a13b07c8770eab88b7a6cd0870c3e36442e4ef4937f36fd0b6d1cea2c", size = 210875, upload-time = "2026-04-10T06:22:02.746Z" }, + { url = "https://files.pythonhosted.org/packages/e2/f1/cf7a49b36385ed1ee0cc7f6b8861904f1533a3286e01cd1e3c2eb72976b9/zope_interface-8.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ec2728e3cf685126ccd2e0f7635fb60edf116f76f402dd66f4df13d9d9348b4b", size = 211199, upload-time = "2026-04-10T06:22:04.596Z" }, + { url = "https://files.pythonhosted.org/packages/cc/86/1ccb73ce9189b1345b7824830a18796ae0b33317d3725d8a034a6ce06501/zope_interface-8.3-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:568b97cb701fd2830b52198a2885e851317a019e1912eaad107860e3cca71964", size = 259885, upload-time = "2026-04-10T06:22:06.403Z" }, + { url = "https://files.pythonhosted.org/packages/a1/de/d0185211ad4902641c0233b7c3b42e21582ffac24f5afe5cc4736b196346/zope_interface-8.3-cp311-cp311-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:62839e4201869a29f99742df7f7139cac4ce301850d3787da37f84e271ad9b95", size = 264308, upload-time = "2026-04-10T06:22:08.425Z" }, + { url = "https://files.pythonhosted.org/packages/0e/e5/ac6f24cdaa04711246d425a2ca301e2f3c97e8d6d672b44258eb2ceb92ff/zope_interface-8.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d287183767926bc9841e51471a28b77c7b49fddf65016aa7faf5a1447e2b6558", size = 265594, upload-time = "2026-04-10T06:22:10.111Z" }, + { url = "https://files.pythonhosted.org/packages/b3/ca/e888c67123b6a7019936c67b5ebcc9396fdb3067cf278d7541d24f4c1a86/zope_interface-8.3-cp311-cp311-win_amd64.whl", hash = "sha256:12a33bb596ca20520e44f97918950cfc66a632ac0278a7f40608217cc4269948", size = 214562, upload-time = "2026-04-10T06:22:12.681Z" }, + { url = "https://files.pythonhosted.org/packages/16/1e/7ed593f9c3664e560febe1f132fdf73b8bb9a3de6e3448093b0167239c8c/zope_interface-8.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b361b7ce566bc024e55f74eb1e88afc14039d7bd8ea13eeff3b7a8400dc59683", size = 211571, upload-time = "2026-04-10T06:22:14.775Z" }, + { url = "https://files.pythonhosted.org/packages/cf/31/844979b472f30efd2a68480738c9a3be518786b0885137075616607e88c7/zope_interface-8.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f5be73ca1304daa3046ee5835f7fa6b3badadf02102b570532dd57cd25dd72d6", size = 211748, upload-time = "2026-04-10T06:22:16.695Z" }, + { url = "https://files.pythonhosted.org/packages/6e/b6/71f5c9d8dde7334e1b67306fea5814c67eac92d871bb0dfc664c9f3355f1/zope_interface-8.3-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:961af756797e36c1e77f7d0dc8ac1322de0c071eaa1a641dbe3b790061968dd9", size = 264718, upload-time = "2026-04-10T06:22:19.473Z" }, + { url = "https://files.pythonhosted.org/packages/94/e3/5eab77fd6795ca37b9ed1aeea5290170018938549322003745bdcd939238/zope_interface-8.3-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a6329f296b70f62043bf2df06eb91b4be040baee32ec4a3e0314f3893fa5c51c", size = 269795, upload-time = "2026-04-10T06:22:21.728Z" }, + { url = "https://files.pythonhosted.org/packages/a2/2f/4bc8807d65833f06335a49beb1786bafcf748cde7472ba14cdb4db463ba8/zope_interface-8.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f420f6c96307ff265981c510782f0ed97475107b78ca9fca0bb04fe36f363eb4", size = 269418, upload-time = "2026-04-10T06:22:23.802Z" }, + { url = "https://files.pythonhosted.org/packages/50/3d/1cfaf770bc6bc64edec3d4c5f17b5dbe600bf93cd2caac5ee0880eb9f9e0/zope_interface-8.3-cp312-cp312-win_amd64.whl", hash = "sha256:ffeae9102aa6ba5bd2f9a547016347bd87c9cf01aea564936c0d165fff0b1242", size = 214390, upload-time = "2026-04-10T06:22:25.735Z" }, + { url = "https://files.pythonhosted.org/packages/27/da/ff205c5463e52ad64cc40be667fdff2b01b9754a385c6b95bac01645fa4f/zope_interface-8.3-cp313-cp313-macosx_10_9_x86_64.whl", hash = "sha256:1aa0e1d72212cedc38b2156bbca08cf24625c057135a7947ef6b19bc732b2772", size = 211889, upload-time = "2026-04-10T06:22:27.612Z" }, + { url = "https://files.pythonhosted.org/packages/c7/21/0cc848e22769b1cf4c0cd636ec2e60ea05cfb958423435ea526d5a291fe8/zope_interface-8.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:54ab83218a8f6947ba4b6cb1a121f1e1abe2e418b838ccdac71639d0f97e734e", size = 211961, upload-time = "2026-04-10T06:22:29.575Z" }, + { url = "https://files.pythonhosted.org/packages/e3/54/815c9dbb90336c50694b4c7ef7ced06bc389e5597200c77457b557a0221c/zope_interface-8.3-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:34d6c10fa790005487c471e0e4ab537b0fa9a70e55a96994e51ffeef92205fa4", size = 264409, upload-time = "2026-04-10T06:22:31.426Z" }, + { url = "https://files.pythonhosted.org/packages/3a/69/2e5c30adde0e94552d934971fa6eba107449d3d11fa086cfcfeb8ea6354d/zope_interface-8.3-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:93108d5f8dee20177a637438bf4df4c6faf8a317c9d4a8b1d5e78123854e3317", size = 269592, upload-time = "2026-04-10T06:22:33.393Z" }, + { url = "https://files.pythonhosted.org/packages/23/8a/fbb1dceb5c5400b2b27934aa102d29fe4cb06732122e7f409efebeb6e097/zope_interface-8.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8f81d90f80b9fbf36602549e2f187861c9d7139837f8c9dd685ce3b933c6360f", size = 269548, upload-time = "2026-04-10T06:22:35.339Z" }, + { url = "https://files.pythonhosted.org/packages/a2/70/abd0bb9cc9b1a9a718f30c81f46a184a2e751dd80cf57db142ffa42730da/zope_interface-8.3-cp313-cp313-win_amd64.whl", hash = "sha256:96106a5f609bb355e1aec6ab0361213c8af0843ca1e1ba9c42eacfbd0910914e", size = 214391, upload-time = "2026-04-10T06:22:36.969Z" }, + { url = "https://files.pythonhosted.org/packages/8a/d9/95fe0d4d8da09042383c42f239e0106f1019ec86a27ed9f5000e754f6e7a/zope_interface-8.3-cp314-cp314-macosx_10_9_x86_64.whl", hash = "sha256:96f0001b49227d756770fc70ecde49f19332ae98ec98e1bbbf2fd7a87e9d4e45", size = 211979, upload-time = "2026-04-10T06:22:38.628Z" }, + { url = "https://files.pythonhosted.org/packages/f3/01/b6f694444ea1c911a4ea915f4ef066a95e9d1a58256a30c131ec88c3ae64/zope_interface-8.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3853bfb808084e1b4a3a769b00bd8b58a52b0c4a4fc5c23de26d283cd8beb627", size = 212038, upload-time = "2026-04-10T06:22:40.475Z" }, + { url = "https://files.pythonhosted.org/packages/f7/cf/237de1fba4f05686bc344eeb035236bd89890679c8211f129f05b5971ccf/zope_interface-8.3-cp314-cp314-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:33a13acba79ef693fb64ceb6193ece913d39586f184797f133c1bc549da86851", size = 266041, upload-time = "2026-04-10T06:22:42.093Z" }, + { url = "https://files.pythonhosted.org/packages/58/5f/df85b1ff5626d7f05231e69b7efd38bdc2c82ca363495e0bb112aaf655b3/zope_interface-8.3-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e9f7e4b46741a11a9e1fab8b68710f08dec700e9f1b877cdca02480fbebe4846", size = 269094, upload-time = "2026-04-10T06:22:43.832Z" }, + { url = "https://files.pythonhosted.org/packages/5f/10/7ad1ff9c514fe38b176fc1271967c453074eb386a4515bd3b957c485f3a8/zope_interface-8.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8ce49d43366e12aeccd14fcaebb3ef110f50f5795e0d4a95383ea057365cedf2", size = 269413, upload-time = "2026-04-10T06:22:45.573Z" }, + { url = "https://files.pythonhosted.org/packages/38/42/3b0b5edee7801e0dd5c42c2c9bb4ec8bec430a6628462eb1315db76a7954/zope_interface-8.3-cp314-cp314-win_amd64.whl", hash = "sha256:301db4049c79a15a3b29d89795e150daf0e9ae701404b112ad6585ea863f6ef5", size = 215170, upload-time = "2026-04-10T06:22:47.115Z" }, ] From 2e256d7f0633ccce23308b6149581530802ae336 Mon Sep 17 00:00:00 2001 From: Lukas Kikuchi Date: Fri, 1 May 2026 12:42:08 +0100 Subject: [PATCH 13/15] docs(netrun): documentation audit for Phases 0-5 features + 3 new samples MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prompt to recreate this work: After completing Phases 0-5 of the major refactor (bug fixes, shared event loop, ctx.state/depends_on/resources, BasePool extraction, CLI extraction with --edges/--format=mermaid/dry-run), audit and update all repo documentation against the current code state. Also add focused sample projects for the three new scheduling/state features. Steps: 1. Delete `netrun/PROJECT_SPEC.md` (file already gone, but scrub all references): remove the line in `netrun/README.md` documentation list, the line in `README.md` documentation list, the `**See netrun/PROJECT_SPEC.md for the full specification.**` line in `CLAUDE.md`, the `│ ├── PROJECT_SPEC.md # Full specification` line in CLAUDE.md's repo structure, and the `# Additional execution options (from PROJECT_SPEC.md)` comment in `netrun/pts/netrun/06_net/00_config/01_nodes.pct.py`. 2. Update `CLAUDE.md`: - Reword the project structure intro to "split into the following components" (was "two main components") and add a `### netrun-cli (CLI)` subsection describing it as a separate package. - Remove "CLI for validation, inspection, and config conversion" bullet from the netrun feature list. - Update the Repository Structure ASCII tree: remove `PROJECT_SPEC.md` line, insert a `netrun-cli/` block under `netrun/` showing the same pts/src structure plus pyproject.toml note. - Remove the CLI entry from "Current Implementation Status" (line "8. **CLI** (`netrun.cli`) - ..."). Renumber remaining items. Add a closing line: "The CLI lives in the separate **`netrun-cli`** package — see "netrun-cli specifics" below." - In the ExecutionManager usage example, remove the `send_channel=False,` line and the `print(result.print_buffer)` line. After the example, add a paragraph about the shared event loop dispatch model and a paragraph about worker crash isolation. In the Protocol Keys list, remove `UP_RUN_STARTED` and `UP_PRINT_BUFFER`; add `UP_SEND_FUNCTION_RESPONSE`. - Augment the "Key Classes" list under Net Module: bold the new fields (`resources` on NetConfig; `depends_on`, `resources` on NodeExecutionConfig; `ctx.state` on NodeExecutionContext). - Append `, scheduling constraints (depends_on, resources), retry-persistent state (ctx.state)` to the Net "Features:" line. - Add a "#### Scheduling: depends_on and resources" subsection (with code example showing net-level `resources={"gpu":1, "http_conn":4}` and node-level `depends_on=["preprocess"], resources={"gpu":1}`, plus mutual-exclusion / bounded-concurrency patterns). - Add a "#### Retry-persistent state: ctx.state" subsection with the `pd.read_parquet` cache example and lifecycle/locality notes. - Delete the entire "### CLI (`netrun.cli`)" section. - Add a new "## netrun-cli specifics" top-level section after "### Core" covering: editing workflow, install/run instructions, a command table listing every command including new ones, and a "Adding a new command" four-step recipe. 3. Update root `README.md`: - In the netrun component description, replace "a CLI" mention with scheduling + ctx.state language. - Add a `### [netrun-cli](netrun-cli/) — CLI` component section after netrun, mentioning it depends on netrun for config models. - Add `netrun-cli/` to the Repository Structure ASCII tree. - Add a "### netrun-cli" Quick Start block with `uv sync`, `.venv/bin/netrun --help`, and example commands against `sample_projects/00_basic_net_project`. - Remove the PROJECT_SPEC.md line from the Documentation list. 4. Update `netrun/README.md`: - Expand the Key Features list: replace the old factories bullet with "function/broadcast/join factories"; add bullets for "Scheduling constraints (depends_on, resources)", "Retry-persistent state (ctx.state)", "Shared event loop", "Worker crash isolation". Drop the CLI bullet. - Add a closing line: "The CLI is shipped as a separate package: see [netrun-cli](../netrun-cli/)." - Remove the PROJECT_SPEC.md line from the Documentation list. 5. Update inline docstrings (.pct.py files; auto-export to .py via nblite): - `netrun/pts/netrun/05_execution_manager.pct.py`: - Add a class-level docstring on `ExecutionManager` covering the single-terminal-event protocol, worker crash isolation, and the shared event loop model (one daemon thread per process scope). - Update `__init__`'s docstring to list pool types as types (`ThreadPool`, `MultiprocessPool`, `SingleWorkerPool`, `RemotePoolClient`), not strings. - Rewrite `run()`'s docstring to emphasise: targets a specific worker, single `UP_RUN_RESPONSE` carries result+timestamps, returns `JobResult`, raises whatever the user fn raises. Drop any mention of `send_channel` or `UP_RUN_STARTED`. - Add a docstring to `run_allocate()` documenting the candidate-list expansion (pool ID -> all workers; tuple -> specific worker), the three allocation methods (ROUND_ROBIN/RANDOM/LEAST_BUSY), and the empty-list ValueError. - `netrun/pts/netrun/06_net/01_net/00_context.pct.py`: replace the `NodeExecutionContext` class docstring with one that explicitly documents `ctx.state` — what it is (per-epoch dict), retry semantics (same instance reused, cleared on success/permanent fail), locality (per worker for multiprocess/remote, picklability requirement), and a `pd.read_parquet`-style example. Note that `state` is NOT deferred unlike packet operations. - `netrun/pts/netrun/06_net/01_net/02_net.pct.py`: expand the `Net` class docstring to enumerate the major responsibilities (pool lifecycle, scheduling via depends_on+resources, retries with ctx.state, timeouts, cache/file-storage replay, output queues, signals/controls, structured logging, lifecycle callbacks). Note the shared event loop model and worker crash isolation. Mention the `async with Net(config) as net:` (and sync) context-manager usage. - `netrun/pts/netrun/06_net/00_config/01_nodes.pct.py`: remove the "# Additional execution options (from PROJECT_SPEC.md)" comment line before the `defer_init` field. Then run `cd netrun && nbl export --reverse && nbl export` to propagate. 6. Add three focused sample projects under `sample_projects/`: - `15_ctx_state/`: Single node `expensive_pipeline` that simulates a 0.5s heavy data load on first attempt, caches it in `ctx.state["heavy"]`, then runs flaky logic that fails on attempt 1 and succeeds on attempt 2 (reading `ctx.state["attempts"]` and incrementing). Config has `retries: 2`. The print logs should show "Loading heavy data" once and "Reusing heavy data from previous attempt — no reload" on retry. Output queue `results` collects `pipeline.out`. - `16_depends_on/`: Three nodes `step_a`, `step_b`, `step_c` with **no data edges** between them. `step_b.execution_config.depends_on = ["step_a"]` and `step_c.execution_config.depends_on = ["step_b"]`. Each node takes a `trigger` parameter. main.py injects `["go"]` to all three triggers up front; output queue `all_done` collects all three `out` ports. Verify the completion order matches `[A, B, C]`. - `17_resources/`: Three sleeping (0.2s) `gpu_job_1/2/3` nodes on a `threads` thread pool with `num_workers: 3`. Net-level `resources: {"gpu": 1}`; each node declares `execution_config.resources: {"gpu": 1}`. Inject all triggers up front. The total wall-clock time must be ~0.6s (serial), not ~0.2s (parallel). Each sample needs a `pyproject.toml` (standard form: `dependencies = ["netrun"]`, `[tool.uv.sources] netrun = { path = "../../netrun", editable = true }`), a `nodes.py`, a `main.netrun.json`, and a `main.py`. Note: in `main.netrun.json`, `output_queues` lives at the **top level**, not nested under `graph`. 7. Run sample 14, 15, 16, 17 with `uv run python main.py` to confirm they work. Confirm: - 15 prints `Result: 499542` (= sum(0..999) + 42 = 499500 + 42) - 16 prints `Completion order: ['A complete', 'B complete', 'C complete']` - 17 reports total wall-clock ~0.6s 8. Run the full test suites: `cd netrun && uv run pytest src/tests/ -q` (1305 pass) and `cd netrun-cli && uv run pytest src/tests/ -q` (61 pass). Co-Authored-By: Claude Opus 4.7 (1M context) --- CLAUDE.md | 150 +- README.md | 22 +- netrun/README.md | 10 +- netrun/pts/netrun/05_execution_manager.pct.py | 70 +- .../netrun/06_net/00_config/01_nodes.pct.py | 1 - .../netrun/06_net/01_net/00_context.pct.py | 28 +- netrun/pts/netrun/06_net/01_net/02_net.pct.py | 15 +- netrun/src/netrun/execution_manager.py | 70 +- netrun/src/netrun/net/_net/_context.py | 28 +- netrun/src/netrun/net/_net/_net.py | 15 +- netrun/src/netrun/net/config/_nodes.py | 1 - sample_projects/15_ctx_state/main.netrun.json | 22 + sample_projects/15_ctx_state/main.py | 28 + sample_projects/15_ctx_state/nodes.py | 40 + sample_projects/15_ctx_state/pyproject.toml | 9 + sample_projects/15_ctx_state/uv.lock | 1235 +++++++++++++++++ .../16_depends_on/main.netrun.json | 43 + sample_projects/16_depends_on/main.py | 28 + sample_projects/16_depends_on/nodes.py | 32 + sample_projects/16_depends_on/pyproject.toml | 9 + sample_projects/16_depends_on/uv.lock | 1235 +++++++++++++++++ sample_projects/17_resources/main.netrun.json | 49 + sample_projects/17_resources/main.py | 36 + sample_projects/17_resources/nodes.py | 36 + sample_projects/17_resources/pyproject.toml | 9 + sample_projects/17_resources/uv.lock | 1235 +++++++++++++++++ 26 files changed, 4388 insertions(+), 68 deletions(-) create mode 100644 sample_projects/15_ctx_state/main.netrun.json create mode 100644 sample_projects/15_ctx_state/main.py create mode 100644 sample_projects/15_ctx_state/nodes.py create mode 100644 sample_projects/15_ctx_state/pyproject.toml create mode 100644 sample_projects/15_ctx_state/uv.lock create mode 100644 sample_projects/16_depends_on/main.netrun.json create mode 100644 sample_projects/16_depends_on/main.py create mode 100644 sample_projects/16_depends_on/nodes.py create mode 100644 sample_projects/16_depends_on/pyproject.toml create mode 100644 sample_projects/16_depends_on/uv.lock create mode 100644 sample_projects/17_resources/main.netrun.json create mode 100644 sample_projects/17_resources/main.py create mode 100644 sample_projects/17_resources/nodes.py create mode 100644 sample_projects/17_resources/pyproject.toml create mode 100644 sample_projects/17_resources/uv.lock diff --git a/CLAUDE.md b/CLAUDE.md index 67c3167c..38f0f9d5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,7 +4,7 @@ This repository contains the **netrun** project, a flow-based development (FBD) ## Project Structure -The project is split into two main components: +The project is split into the following components: ### netrun-sim (Simulation Engine) @@ -21,17 +21,20 @@ This separation of concerns allows the actual execution and data storage to be i - Worker pool management (threads, processes, remote) - High-level execution orchestration via `ExecutionManager` - Node factories for creating nodes from functions or broadcast patterns -- CLI for validation, inspection, and config conversion - Tools for template resolution, action execution, and recipe management -**See `netrun/PROJECT_SPEC.md` for the full specification.** - **Important:** The `netrun` package uses **nblite** for literate programming. Before writing any code for `netrun`, you **must** read `netrun/NBLITE_INSTRUCTIONS.md` carefully. Key points: - Source code lives in `.pct.py` files (percent notebooks), not in the exported Python modules - Never edit files in `src/netrun/` directly - they are auto-generated - After editing `.pct.py` files, run `nbl export --reverse` then `nbl export` - **nblite does NOT auto-generate `__init__.py` files.** You must create `__init__.py` files manually in `src/` for any package that needs re-exports. These manual `__init__.py` files are not overwritten by `nbl export`. +### netrun-cli (CLI) + +`netrun-cli` is a separate package that provides the `netrun` command-line tool. It depends on `netrun` for config models and tools, but is shipped independently so the runtime stays free of CLI dependencies (typer, tomli-w). + +It uses the same nblite workflow as `netrun`. Source files live in `netrun-cli/pts/netrun_cli/`. The package installs the `netrun` command via `[project.scripts]`. See **netrun-cli specifics** below for details. + ### netrun-ui (Visual Editor) `netrun-ui` is a visual editor for creating and editing netrun flow configurations. The frontend is built with SvelteKit (Svelte 5) and SvelteFlow. The backend is a FastAPI Python app (`netrun_ui_backend/`) that handles file I/O, factory resolution, config validation, and action execution. @@ -89,7 +92,6 @@ repo/ │ │ └── netrun_sim/ │ └── examples/ # Python examples ├── netrun/ # Runtime (pure Python, nblite project) -│ ├── PROJECT_SPEC.md # Full specification │ ├── NBLITE_INSTRUCTIONS.md # How to write code (READ THIS FIRST) │ ├── nblite.toml # nblite configuration │ ├── nbs/ # Jupyter notebooks (.ipynb) @@ -101,6 +103,15 @@ repo/ │ └── src/ # Auto-generated code (DO NOT EDIT) │ ├── netrun/ # Generated Python package │ └── tests/ # Generated test files +├── netrun-cli/ # CLI (separate package, nblite project) +│ ├── nblite.toml # nblite configuration +│ ├── pyproject.toml # Defines `netrun` command via [project.scripts] +│ ├── pts/ +│ │ ├── netrun_cli/ # CLI source percent notebooks +│ │ └── tests/ # CLI test percent notebooks +│ └── src/ +│ ├── netrun_cli/ # Generated CLI package +│ └── tests/ # Generated CLI tests └── netrun-ui/ # Visual editor (SvelteKit + FastAPI) ├── src/ # Frontend source (Svelte 5, SvelteFlow) └── netrun_ui_backend/ # Python backend (FastAPI) @@ -120,10 +131,11 @@ All major modules are fully implemented: 4. **RPC Layer** (`netrun.rpc`) - Bidirectional message-passing channels 5. **Pool Layer** (`netrun.pool`) - Worker pool management 6. **ExecutionManager** (`netrun.execution_manager`) - High-level execution orchestration -7. **Node Factories** (`netrun.node_factories`) - Function and broadcast factories -8. **CLI** (`netrun.cli`) - Validation, inspection, conversion commands -9. **Tools** (`netrun.tools`) - Template resolution, action execution, recipes -10. **Core** (`netrun.core`) - Convenience re-exports of Net, NetConfig, etc. +7. **Node Factories** (`netrun.node_factories`) - Function, broadcast, join factories +8. **Tools** (`netrun.tools`) - Template resolution, action execution, recipes +9. **Core** (`netrun.core`) - Convenience re-exports of Net, NetConfig, etc. + +The CLI lives in the separate **`netrun-cli`** package — see "netrun-cli specifics" below. --- @@ -253,21 +265,22 @@ async with manager: pool_id="thread_pool", worker_id=0, func_import_path_or_key="my_func", - send_channel=False, func_args=(1, 2), func_kwargs={"x": 3}, ) print(result.result) # Function return value - print(result.print_buffer) # Captured print statements ``` +**Async dispatch model**: Async functions dispatched to thread/multiprocess workers run on a single shared event loop owned by the manager (one daemon thread per process scope). Sync functions run directly on the worker. This makes `asyncio.create_subprocess_exec`, nested awaits, and cross-node `asyncio.Lock` work without per-worker loop hacks. + +**Worker crash isolation**: A crashed worker only fails the jobs targeting it; the rest of the pool stays alive. + **ExecutionManager Protocol Keys**: -- `RUN` - Execute a function -- `SEND_FUNCTION` - Register a function by key -- `UP_RUN_STARTED` - Confirmation function started -- `UP_RUN_RESPONSE` - Return result -- `UP_PRINT_BUFFER` - Captured print statements +- `RUN` - Execute a function on a worker +- `SEND_FUNCTION` - Register a function by key on a worker +- `UP_RUN_RESPONSE` - Single terminal event with result and timestamps +- `UP_SEND_FUNCTION_RESPONSE` - Acknowledgment of SEND_FUNCTION ### Net Module (`netrun.net`) @@ -275,15 +288,15 @@ The Net module provides flow-based network execution by integrating with netrun- **Key Classes**: - `Net` - Main orchestrator: manages pools, executes epochs, routes packets -- `NetConfig` - Top-level configuration (pools, graph, output queues, storage) +- `NetConfig` - Top-level configuration (pools, graph, output queues, storage, **`resources`**) - `NodeConfig` - Node definition (ports, salvo conditions, factory, execution config) -- `NodeExecutionConfig` - Execution settings (pools, retries, rate limiting, type checking) +- `NodeExecutionConfig` - Execution settings (pools, retries, type checking, **`depends_on`**, **`resources`**) - `GraphConfig` - Graph topology (nodes, edges, output queues) - `PortConfig` - Port definition with optional type annotation - `EdgeConfig` - Connection between ports (`source_node`, `source_port`, `target_node`, `target_port`) - `SalvoConditionConfig` - Rules for triggering epochs or sending output - `OutputQueueConfig` - Collects packets from unconnected output ports -- `NodeExecutionContext` - Context passed to node functions (consume/create packets, print, etc.) +- `NodeExecutionContext` - Context passed to node functions (`ctx.create_packet`, `ctx.print`, **`ctx.state`**, etc.) - `TargetInputSalvo` - Target-based execution support **Net Lifecycle**: @@ -321,19 +334,49 @@ remove = net.on_epoch_end(callback, node="fetch") Both sync and async callbacks are supported. `on_epoch_end` receives the `EpochLog` (with `was_cancelled`, `ended_at`, `started_at`, `logs`, etc.). -**Features**: signals, controls, pause/resume, rate limiting, retries, type checking, print capture, output queues, caching, file storage, epoch lifecycle callbacks. +**Features**: signals, controls, pause/resume, rate limiting, retries, type checking, print capture, output queues, caching, file storage, epoch lifecycle callbacks, scheduling constraints (`depends_on`, `resources`), retry-persistent state (`ctx.state`). + +#### Scheduling: `depends_on` and `resources` + +`NodeExecutionConfig.depends_on: list[str] | None` — Names of nodes that must complete at least one epoch before this node can fire. Provides directional ordering without requiring data edges. Validated at Net construction: cycles and references to non-existent nodes raise `ValueError`. -### CLI (`netrun.cli`) +`NodeExecutionConfig.resources: dict[str, int] | None` — Named semaphore-style requirements. The node acquires the listed slots before its epoch starts and releases them when the epoch finishes, fails, or is cancelled. Resources must be declared at the net level via `NetConfig.resources: dict[str, int]` (resource name → total capacity). -Command-line interface (Typer-based) for working with netrun configs: +```python +# Net-level capacities +NetConfig( + resources={"gpu": 1, "http_conn": 4}, + graph=GraphConfig(nodes=[...]), +) + +# Node-level requirements +NodeConfig( + name="train", + execution_config=NodeExecutionConfig( + pools=["main"], + depends_on=["preprocess"], # waits for preprocess to complete first + resources={"gpu": 1}, # holds the GPU slot during epoch + ), +) +``` -- `validate` - Validate a config file -- `structure` - Output graph topology as JSON -- `convert` - Convert between config formats (JSON/TOML) -- `factory-info` - Show factory parameters and ports -- `info` - Show net information -- `nodes` - List all nodes -- `node` - Show specific node details +Common patterns: +- **Mutual exclusion** between two nodes: declare a 1-slot resource and have both nodes require 1 slot. +- **Bounded concurrency** (e.g. max 4 HTTP connections): declare a 4-slot resource and have N HTTP nodes each require 1 slot. + +#### Retry-persistent state: `ctx.state` + +`NodeExecutionContext.state: dict[str, Any]` — Mutable dict that persists across retry attempts of the same epoch. Use it to cache expensive precomputation so retries don't redo the work. + +```python +async def main(ctx, ad_ids): + if "texts_df" not in ctx.state: + ctx.state["texts_df"] = pd.read_parquet(big_path) # loaded once, even on retry + texts_df = ctx.state["texts_df"] + ... +``` + +Lifecycle: cleared when the epoch succeeds or permanently fails. The dict lives wherever the node runs — for multiprocess / remote pools, each worker process keeps its own copy and values must be picklable. ### Tools (`netrun.tools`) @@ -349,6 +392,55 @@ Utilities for action execution and recipe management: Convenience re-exports of the most commonly used classes: `Net`, `NetConfig`, `NodeConfig`, `NodeExecutionConfig`, `PoolConfig`, `PortConfig`, `EdgeConfig`, `GraphConfig`, `SalvoConditionConfig`, `OutputQueueConfig`, `TargetInputSalvo`, `EnvVar`. +--- + +## netrun-cli specifics + +The CLI is a separate package at `netrun-cli/`. Source files live in `netrun-cli/pts/netrun_cli/`; tests live in `netrun-cli/pts/tests/`. It uses the same nblite workflow as `netrun`. + +**Editing the CLI:** +1. Edit `.pct.py` files in `netrun-cli/pts/netrun_cli/` or `netrun-cli/pts/tests/` +2. From `netrun-cli/`: `nbl export --reverse && nbl export` +3. Run tests: `cd netrun-cli && uv run pytest src/tests/ -v` + +**Installing the CLI command:** + +```bash +cd netrun-cli && uv sync +.venv/bin/netrun --help +``` + +The `netrun` script is registered via `[project.scripts]` in `netrun-cli/pyproject.toml` and points to `netrun_cli._app:app_main`. `netrun-cli` depends on `netrun` (editable in dev) for config models and tools. + +**Available commands:** + +| Command | Description | +|---------|-------------| +| `validate` | Validate a config file (catches resolution failures) | +| `structure` | Output graph topology (`--format json` or `--format mermaid`) | +| `convert` | Convert between `.netrun.json` and `.netrun.toml` | +| `factory-info ` | Inspect a factory's parameters | +| `info` | Summary stats (nodes, edges, pools, factories, recipes) | +| `nodes` | List all nodes with port names | +| `node ` | Detailed node info; pass `--edges` to include incoming/outgoing edges | +| `dry-run` | Show topological execution order, source/sink nodes, and scheduling constraints | +| `add-node` / `remove-node` / `edit-node` | Mutate graph nodes | +| `add-edge` / `remove-edge` | Mutate edges | +| `actions list/run` | Inspect and run actions | +| `recipes list/run` | Inspect and run recipes | +| `download-agents` | Pull agent docs from the netrun GitHub repo | + +**Adding a new command:** + +1. Add a Typer command function to one of `netrun-cli/pts/netrun_cli/0X_*.pct.py` (or create a new module) +2. Register it in `00_app.pct.py` via `app.command("name")(fn)` +3. Add tests to `netrun-cli/pts/tests/test_cli.pct.py` +4. Re-export and run tests + +The CLI imports from `netrun.net.config`, `netrun.tools`, etc. — these are already external dependencies through the `netrun` package, so no special setup is required. + +--- + ### Node Variables (`NodeVariable`) Node variables are typed key-value pairs accessible to nodes via `ctx.vars`. They support a two-level inheritance model: diff --git a/README.md b/README.md index 3a3a77a3..19b4c338 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,11 @@ A flow-based development (FBD) runtime system. Define networks of interconnected ### [netrun](netrun/) — Runtime -A pure Python package built on netrun-sim. Provides flow-based network execution (`Net`), RPC channels, worker pools (thread/process/remote), node factories, caching, file storage, a CLI, and more. +A pure Python package built on netrun-sim. Provides flow-based network execution (`Net`), RPC channels, worker pools (thread/process/remote), node factories, caching, file storage, scheduling constraints (`depends_on`, `resources`), and retry-persistent state (`ctx.state`). + +### [netrun-cli](netrun-cli/) — CLI + +A separate Python package providing the `netrun` command-line tool. Validates configs, inspects graphs, converts formats, runs actions and recipes, mutates graphs, and emits Mermaid topology diagrams. Depends on `netrun` for config models. ### [netrun-sim](netrun-sim/) — Simulation Engine @@ -26,6 +30,9 @@ repo/ ├── netrun/ # Runtime (pure Python, nblite project) │ ├── pts/ # Source code (.pct.py files) │ └── src/ # Auto-generated Python modules +├── netrun-cli/ # CLI (separate Python package, nblite) +│ ├── pts/ # Source code (.pct.py files) +│ └── src/ # Auto-generated Python modules └── netrun-ui/ # Visual editor ├── src/ # Frontend (Svelte 5, SvelteFlow) └── netrun_ui_backend/ # Backend (FastAPI) @@ -59,6 +66,18 @@ uv sync uv run pytest src/tests/ -v ``` +### netrun-cli + +```bash +cd netrun-cli +uv sync +.venv/bin/netrun --help + +# Try it on a sample project +.venv/bin/netrun structure --format mermaid -c ../sample_projects/00_basic_net_project/main.netrun.json +.venv/bin/netrun dry-run -c ../sample_projects/00_basic_net_project/main.netrun.json +``` + ### netrun-ui ```bash @@ -85,7 +104,6 @@ netrun-ui --dev - [netrun-sim/README.md](netrun-sim/README.md) — Simulation engine docs - [netrun/README.md](netrun/README.md) — Runtime docs - [netrun-ui/README.md](netrun-ui/README.md) — Visual editor docs -- [netrun/PROJECT_SPEC.md](netrun/PROJECT_SPEC.md) — Runtime specification ## License diff --git a/netrun/README.md b/netrun/README.md index 3c7ccbd0..9dbd2af4 100644 --- a/netrun/README.md +++ b/netrun/README.md @@ -6,14 +6,19 @@ A flow-based development (FBD) runtime for Python. Define networks of interconne - **Flow-based execution** — Define graphs of nodes connected by ports and edges; packets flow automatically based on salvo conditions - **Multiple pool types** — Execute nodes across threads, processes, or remote workers via WebSockets -- **Node factories** — Create nodes from regular Python functions (`function` factory) or fan-out patterns (`broadcast` factory) +- **Node factories** — Create nodes from regular Python functions (`function` factory), fan-out patterns (`broadcast`), and joins (`join`) +- **Scheduling constraints** — `depends_on` for node ordering, `resources` for semaphore-style coordination (mutual exclusion, bounded concurrency) +- **Retry-persistent state** — `ctx.state` dict survives retries so expensive precomputation isn't redone +- **Shared event loop** — Async nodes run on a single asyncio loop per process; subprocess calls and cross-node `asyncio.Lock` work cleanly +- **Worker crash isolation** — A crashed worker fails only its own jobs; the pool stays alive - **Signals and controls** — Pause, resume, and control network execution at runtime - **Caching and storage** — Cache epoch results, store packet data to local/S3/SSH/GCS backends - **Node variables** — Typed, inheritable configuration variables with environment variable support - **Output queues** — Collect results from terminal nodes -- **CLI** — Validate configs, inspect graphs, convert formats, and query factory info - **Config formats** — Define networks in JSON or TOML with `NetConfig.from_file()` +The CLI is shipped as a separate package: see [netrun-cli](../netrun-cli/). + ## Installation ```bash @@ -66,7 +71,6 @@ asyncio.run(main()) ## Documentation - [CLAUDE.md](../CLAUDE.md) — Full module documentation and API reference -- [PROJECT_SPEC.md](PROJECT_SPEC.md) — Detailed project specification - [NBLITE_INSTRUCTIONS.md](NBLITE_INSTRUCTIONS.md) — How to write code (nblite workflow) ## Development diff --git a/netrun/pts/netrun/05_execution_manager.pct.py b/netrun/pts/netrun/05_execution_manager.pct.py index ad35b870..9c573cad 100644 --- a/netrun/pts/netrun/05_execution_manager.pct.py +++ b/netrun/pts/netrun/05_execution_manager.pct.py @@ -454,14 +454,33 @@ class RunAllocationMethod(Enum): PoolType = ThreadPool | MultiprocessPool | SingleWorkerPool | RemotePoolClient class ExecutionManager: + """High-level orchestrator for executing functions across pools. + + Wraps one or more pools (`ThreadPool`, `MultiprocessPool`, `SingleWorkerPool`, + `RemotePoolClient`) and routes function execution requests to specific + workers, tracking per-job lifecycle. + + Each submitted job produces exactly one terminal event (`UP_RUN_RESPONSE`), + which carries the result, both timestamps (started, completed), and any + captured print output. Worker crashes are isolated: a crashed worker only + fails the jobs targeting it, leaving the rest of the pool intact. + + Async functions dispatched to thread/multiprocess workers run on a single + **shared event loop** owned by the manager (one daemon thread per process + scope). Sync functions run directly on the worker thread/process. This + makes `asyncio.create_subprocess_exec`, nested awaits, and cross-node + `asyncio.Lock` work without per-worker loop hacks. + """ + def __init__(self, pool_configs: dict[str, tuple[type[PoolType], dict[str, Any]]]): """ Create an ExecutionManager with the given pool configurations. Args: pool_configs: A dictionary mapping pool_id to (pool_type, pool_init_kwargs). - pool_type can be "thread", "multiprocess", "remote", or "main". - pool_init_kwargs are passed to the pool constructor (excluding worker_fn). + pool_type can be `ThreadPool`, `MultiprocessPool`, `SingleWorkerPool`, + or `RemotePoolClient`. pool_init_kwargs are passed to the pool + constructor (excluding worker_fn). """ self._pool_configs = pool_configs self._pools: dict[str, PoolType] = {} @@ -647,17 +666,30 @@ async def run( func_kwargs, ) -> JobResult: """ - Run a function in a pool. + Run a function on a specific worker and wait for the result. + + Sends a `RUN` request to the target worker and waits for the single + `UP_RUN_RESPONSE` terminal event, which carries the result and timing + metadata. Worker crashes are surfaced as exceptions on the affected + job without disturbing the rest of the pool. Args: - pool_id: The ID of the pool to run the function in. - worker_id: The ID of the worker to run the function on. - func_import_path_or_key: The import path or key of the function to run (for the latter, use send_function to register the function first) - func_args: The arguments to pass to the function. - func_kwargs: The keyword arguments to pass to the function. + pool_id: The pool to run on. + worker_id: The specific worker (within the pool) to run on. + func_import_path_or_key: Either an importable dotted path + (e.g. `"my_pkg.my_func"`), or a key previously registered via + `send_function()` / `send_function_to_pool()`. + func_args: Positional arguments tuple. + func_kwargs: Keyword arguments dict. Returns: - The result of the function. + A `JobResult` with submitted/started/completed timestamps, the + return value, pool/worker IDs, and a flag indicating whether the + result was string-converted (for non-serializable returns from + multiprocess/remote pools). + + Raises: + Whatever the user function raises, propagated to the caller. """ pool = self._pools[pool_id] @@ -723,6 +755,26 @@ async def run_allocate( func_args, func_kwargs, ) -> JobResult: + """ + Pick a worker from the candidate list using `allocation_method`, then run. + + Args: + pool_worker_ids: Candidate workers. Each entry is either a pool ID + (which expands to all workers in that pool) or a `(pool_id, + worker_id)` tuple targeting a specific worker. + allocation_method: `ROUND_ROBIN` (rotates across candidates), + `RANDOM` (uniform pick), or `LEAST_BUSY` (worker with fewest + in-flight jobs; ties broken by round-robin). + func_import_path_or_key: See `run()`. + func_args: Positional arguments tuple. + func_kwargs: Keyword arguments dict. + + Returns: + A `JobResult` from the selected worker. + + Raises: + ValueError: If the candidate list is empty. + """ worker_ids: list[tuple[str, int]] = [] # Convert pool_worker_ids to a list of (pool_id, worker_id) tuples for _id in pool_worker_ids: diff --git a/netrun/pts/netrun/06_net/00_config/01_nodes.pct.py b/netrun/pts/netrun/06_net/00_config/01_nodes.pct.py index 56a3d3a2..26acf6c4 100644 --- a/netrun/pts/netrun/06_net/00_config/01_nodes.pct.py +++ b/netrun/pts/netrun/06_net/00_config/01_nodes.pct.py @@ -257,7 +257,6 @@ class NodeExecutionConfig(VarResolvableModel): close_node_func: NodeStopFunc | str | VarRef | None = Field(default=None, description="Function called when the node closes.") on_node_failure: OnNodeFailureFunc | str | VarRef | None = Field(default=None, description="Callback when node execution fails.") - # Additional execution options (from PROJECT_SPEC.md) defer_init: bool | VarRef = Field(default=False, description="Defer init_node_func until the node's first epoch instead of during Net.init().") run_on_init: bool | VarRef = Field(default=False, description="Execute this node once during Net.init(). Requires a satisfied input salvo condition with zero input packets.") diff --git a/netrun/pts/netrun/06_net/01_net/00_context.pct.py b/netrun/pts/netrun/06_net/01_net/00_context.pct.py index c02b6f65..cc9ee01b 100644 --- a/netrun/pts/netrun/06_net/01_net/00_context.pct.py +++ b/netrun/pts/netrun/06_net/01_net/00_context.pct.py @@ -375,11 +375,29 @@ def __str__(self) -> str: class NodeExecutionContext: """Context object passed to node execution functions. - This is the primary interface for nodes to interact with the Net during execution. - All operations are deferred and committed when the function completes. - - Packet operations (create, consume, load_output_port, send_output_salvo) are - queued locally and executed atomically when the epoch completes successfully. + Primary interface for nodes to interact with the Net during execution. + Most operations (create/consume packets, load output ports, send output + salvos) are deferred and committed atomically when the epoch finishes + successfully — so a partially-completed function can fail without leaving + half-applied side effects in the graph. + + Mutable retry state via `ctx.state`: + `state` is a per-epoch dict that persists across retry attempts. Use it + to cache expensive precomputation (e.g. loaded DataFrames, parsed + configs) so retries don't redo the work. The same dict instance is + reused on every retry; it is cleared when the epoch succeeds or + permanently fails. Unlike the deferred packet operations, mutations + to `state` survive failed attempts. + + Locality: the dict lives wherever the node runs (thread, process, or + remote worker). For multiprocess/remote pools, each worker process + keeps its own copy — values must be picklable if you set them. + + Example: + async def main(ctx, ad_ids): + if "texts" not in ctx.state: + ctx.state["texts"] = pd.read_parquet(big_path) # only on first try + ... """ # Identity diff --git a/netrun/pts/netrun/06_net/01_net/02_net.pct.py b/netrun/pts/netrun/06_net/01_net/02_net.pct.py index b97293c4..aec9309b 100644 --- a/netrun/pts/netrun/06_net/01_net/02_net.pct.py +++ b/netrun/pts/netrun/06_net/01_net/02_net.pct.py @@ -394,8 +394,19 @@ def print_all(self, include_timestamps: bool = True, chronological: bool = False class Net: """Main orchestrator for flow-based network execution. - The Net class bridges netrun-sim (packet flow simulation) with actual node - function execution via ExecutionManager. + Bridges netrun-sim (packet flow simulation) with actual node function + execution via ExecutionManager. Owns pool lifecycle, scheduling + (`depends_on`, `resources`), retries (with persistent `ctx.state`), + timeouts, cache and file-storage replay, output queues, signals/controls, + structured logging, and lifecycle callbacks. + + Async nodes execute on a single shared event loop owned by the + ExecutionManager (one daemon thread per process scope). Sync nodes run + directly on the worker's thread/process. Worker crashes are isolated to + the affected jobs. + + Use `async with Net(config) as net:` (or the sync `with` variant). The + context manager calls `init()` / `close()` automatically. """ def __init__(self, config: NetConfig, run_init_nodes: bool = True): diff --git a/netrun/src/netrun/execution_manager.py b/netrun/src/netrun/execution_manager.py index b54161ff..5b438ac9 100644 --- a/netrun/src/netrun/execution_manager.py +++ b/netrun/src/netrun/execution_manager.py @@ -399,14 +399,33 @@ class RunAllocationMethod(Enum): PoolType = ThreadPool | MultiprocessPool | SingleWorkerPool | RemotePoolClient class ExecutionManager: + """High-level orchestrator for executing functions across pools. + + Wraps one or more pools (`ThreadPool`, `MultiprocessPool`, `SingleWorkerPool`, + `RemotePoolClient`) and routes function execution requests to specific + workers, tracking per-job lifecycle. + + Each submitted job produces exactly one terminal event (`UP_RUN_RESPONSE`), + which carries the result, both timestamps (started, completed), and any + captured print output. Worker crashes are isolated: a crashed worker only + fails the jobs targeting it, leaving the rest of the pool intact. + + Async functions dispatched to thread/multiprocess workers run on a single + **shared event loop** owned by the manager (one daemon thread per process + scope). Sync functions run directly on the worker thread/process. This + makes `asyncio.create_subprocess_exec`, nested awaits, and cross-node + `asyncio.Lock` work without per-worker loop hacks. + """ + def __init__(self, pool_configs: dict[str, tuple[type[PoolType], dict[str, Any]]]): """ Create an ExecutionManager with the given pool configurations. Args: pool_configs: A dictionary mapping pool_id to (pool_type, pool_init_kwargs). - pool_type can be "thread", "multiprocess", "remote", or "main". - pool_init_kwargs are passed to the pool constructor (excluding worker_fn). + pool_type can be `ThreadPool`, `MultiprocessPool`, `SingleWorkerPool`, + or `RemotePoolClient`. pool_init_kwargs are passed to the pool + constructor (excluding worker_fn). """ self._pool_configs = pool_configs self._pools: dict[str, PoolType] = {} @@ -592,17 +611,30 @@ async def run( func_kwargs, ) -> JobResult: """ - Run a function in a pool. + Run a function on a specific worker and wait for the result. + + Sends a `RUN` request to the target worker and waits for the single + `UP_RUN_RESPONSE` terminal event, which carries the result and timing + metadata. Worker crashes are surfaced as exceptions on the affected + job without disturbing the rest of the pool. Args: - pool_id: The ID of the pool to run the function in. - worker_id: The ID of the worker to run the function on. - func_import_path_or_key: The import path or key of the function to run (for the latter, use send_function to register the function first) - func_args: The arguments to pass to the function. - func_kwargs: The keyword arguments to pass to the function. + pool_id: The pool to run on. + worker_id: The specific worker (within the pool) to run on. + func_import_path_or_key: Either an importable dotted path + (e.g. `"my_pkg.my_func"`), or a key previously registered via + `send_function()` / `send_function_to_pool()`. + func_args: Positional arguments tuple. + func_kwargs: Keyword arguments dict. Returns: - The result of the function. + A `JobResult` with submitted/started/completed timestamps, the + return value, pool/worker IDs, and a flag indicating whether the + result was string-converted (for non-serializable returns from + multiprocess/remote pools). + + Raises: + Whatever the user function raises, propagated to the caller. """ pool = self._pools[pool_id] @@ -668,6 +700,26 @@ async def run_allocate( func_args, func_kwargs, ) -> JobResult: + """ + Pick a worker from the candidate list using `allocation_method`, then run. + + Args: + pool_worker_ids: Candidate workers. Each entry is either a pool ID + (which expands to all workers in that pool) or a `(pool_id, + worker_id)` tuple targeting a specific worker. + allocation_method: `ROUND_ROBIN` (rotates across candidates), + `RANDOM` (uniform pick), or `LEAST_BUSY` (worker with fewest + in-flight jobs; ties broken by round-robin). + func_import_path_or_key: See `run()`. + func_args: Positional arguments tuple. + func_kwargs: Keyword arguments dict. + + Returns: + A `JobResult` from the selected worker. + + Raises: + ValueError: If the candidate list is empty. + """ worker_ids: list[tuple[str, int]] = [] # Convert pool_worker_ids to a list of (pool_id, worker_id) tuples for _id in pool_worker_ids: diff --git a/netrun/src/netrun/net/_net/_context.py b/netrun/src/netrun/net/_net/_context.py index 20435141..5aa29ac8 100644 --- a/netrun/src/netrun/net/_net/_context.py +++ b/netrun/src/netrun/net/_net/_context.py @@ -338,11 +338,29 @@ def __str__(self) -> str: class NodeExecutionContext: """Context object passed to node execution functions. - This is the primary interface for nodes to interact with the Net during execution. - All operations are deferred and committed when the function completes. - - Packet operations (create, consume, load_output_port, send_output_salvo) are - queued locally and executed atomically when the epoch completes successfully. + Primary interface for nodes to interact with the Net during execution. + Most operations (create/consume packets, load output ports, send output + salvos) are deferred and committed atomically when the epoch finishes + successfully — so a partially-completed function can fail without leaving + half-applied side effects in the graph. + + Mutable retry state via `ctx.state`: + `state` is a per-epoch dict that persists across retry attempts. Use it + to cache expensive precomputation (e.g. loaded DataFrames, parsed + configs) so retries don't redo the work. The same dict instance is + reused on every retry; it is cleared when the epoch succeeds or + permanently fails. Unlike the deferred packet operations, mutations + to `state` survive failed attempts. + + Locality: the dict lives wherever the node runs (thread, process, or + remote worker). For multiprocess/remote pools, each worker process + keeps its own copy — values must be picklable if you set them. + + Example: + async def main(ctx, ad_ids): + if "texts" not in ctx.state: + ctx.state["texts"] = pd.read_parquet(big_path) # only on first try + ... """ # Identity diff --git a/netrun/src/netrun/net/_net/_net.py b/netrun/src/netrun/net/_net/_net.py index d4666230..5c4e17c0 100644 --- a/netrun/src/netrun/net/_net/_net.py +++ b/netrun/src/netrun/net/_net/_net.py @@ -368,8 +368,19 @@ def print_all(self, include_timestamps: bool = True, chronological: bool = False class Net: """Main orchestrator for flow-based network execution. - The Net class bridges netrun-sim (packet flow simulation) with actual node - function execution via ExecutionManager. + Bridges netrun-sim (packet flow simulation) with actual node function + execution via ExecutionManager. Owns pool lifecycle, scheduling + (`depends_on`, `resources`), retries (with persistent `ctx.state`), + timeouts, cache and file-storage replay, output queues, signals/controls, + structured logging, and lifecycle callbacks. + + Async nodes execute on a single shared event loop owned by the + ExecutionManager (one daemon thread per process scope). Sync nodes run + directly on the worker's thread/process. Worker crashes are isolated to + the affected jobs. + + Use `async with Net(config) as net:` (or the sync `with` variant). The + context manager calls `init()` / `close()` automatically. """ def __init__(self, config: NetConfig, run_init_nodes: bool = True): diff --git a/netrun/src/netrun/net/config/_nodes.py b/netrun/src/netrun/net/config/_nodes.py index ae5db37c..2cd60670 100644 --- a/netrun/src/netrun/net/config/_nodes.py +++ b/netrun/src/netrun/net/config/_nodes.py @@ -222,7 +222,6 @@ class NodeExecutionConfig(VarResolvableModel): close_node_func: NodeStopFunc | str | VarRef | None = Field(default=None, description="Function called when the node closes.") on_node_failure: OnNodeFailureFunc | str | VarRef | None = Field(default=None, description="Callback when node execution fails.") - # Additional execution options (from PROJECT_SPEC.md) defer_init: bool | VarRef = Field(default=False, description="Defer init_node_func until the node's first epoch instead of during Net.init().") run_on_init: bool | VarRef = Field(default=False, description="Execute this node once during Net.init(). Requires a satisfied input salvo condition with zero input packets.") diff --git a/sample_projects/15_ctx_state/main.netrun.json b/sample_projects/15_ctx_state/main.netrun.json new file mode 100644 index 00000000..275161df --- /dev/null +++ b/sample_projects/15_ctx_state/main.netrun.json @@ -0,0 +1,22 @@ +{ + "pools": { + "main": {"spec": {"type": "main"}} + }, + "output_queues": { + "results": {"ports": [["pipeline", "out"]]} + }, + "graph": { + "nodes": [ + { + "name": "pipeline", + "factory": "netrun.node_factories.from_function", + "factory_args": {"func": "nodes.expensive_pipeline"}, + "execution_config": { + "pools": ["main"], + "retries": 2 + } + } + ], + "edges": [] + } +} diff --git a/sample_projects/15_ctx_state/main.py b/sample_projects/15_ctx_state/main.py new file mode 100644 index 00000000..69db4bf7 --- /dev/null +++ b/sample_projects/15_ctx_state/main.py @@ -0,0 +1,28 @@ +"""Run the ctx.state sample. + +Demonstrates that data cached in `ctx.state` survives a failed attempt and +is reused on retry, so expensive precomputation isn't redone. +""" + +import asyncio +from pathlib import Path + +from netrun.core import Net, NetConfig + + +async def main(): + config = NetConfig.from_file(Path(__file__).parent / "main.netrun.json") + async with Net(config) as net: + net.inject_data("pipeline", "x", [42]) + await net.run_until_blocked() + + results = net.flush_output_queue("results") + print("\nResults:", results) + + print("\nNode logs:") + for ts, msg in net.logs.for_node("pipeline"): + print(f" {ts.strftime('%H:%M:%S.%f')[:-3]} | {msg}", end="") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/sample_projects/15_ctx_state/nodes.py b/sample_projects/15_ctx_state/nodes.py new file mode 100644 index 00000000..44c4798f --- /dev/null +++ b/sample_projects/15_ctx_state/nodes.py @@ -0,0 +1,40 @@ +"""Node demonstrating ctx.state for retry-persistent caching. + +The expensive_pipeline node simulates a workflow that: +1. Loads heavy data (slow on the first attempt) +2. Then runs flaky logic that fails on the first try and succeeds on retry + +Without ctx.state, retry #2 would reload the heavy data — wasted work. +With ctx.state, the loaded data is cached on the same dict instance reused +across retries, so the second attempt skips straight to the flaky step. +""" + +import time + + +def _load_heavy_data() -> list[int]: + """Simulates a slow / expensive data load.""" + time.sleep(0.5) + return list(range(1000)) + + +def expensive_pipeline(ctx, x: int, print) -> int: + # First-attempt-only work cached in ctx.state. + # Same dict instance survives across retries; cleared on success/permanent fail. + if "heavy" not in ctx.state: + print("Loading heavy data (slow)...") + ctx.state["heavy"] = _load_heavy_data() + else: + print("Reusing heavy data from previous attempt — no reload.") + + heavy = ctx.state["heavy"] + + # Flaky logic: fail on attempt 1, succeed on attempt 2. + attempt = ctx.state.get("attempts", 0) + 1 + ctx.state["attempts"] = attempt + print(f"Attempt #{attempt}") + + if attempt == 1: + raise RuntimeError("Transient failure on first attempt") + + return sum(heavy) + x diff --git a/sample_projects/15_ctx_state/pyproject.toml b/sample_projects/15_ctx_state/pyproject.toml new file mode 100644 index 00000000..207fd9b4 --- /dev/null +++ b/sample_projects/15_ctx_state/pyproject.toml @@ -0,0 +1,9 @@ +[project] +name = "ctx-state-sample" +version = "0.1.0" +description = "Demonstrates ctx.state for retry-persistent caching" +requires-python = ">=3.11" +dependencies = ["netrun"] + +[tool.uv.sources] +netrun = { path = "../../netrun", editable = true } diff --git a/sample_projects/15_ctx_state/uv.lock b/sample_projects/15_ctx_state/uv.lock new file mode 100644 index 00000000..4b5eaba3 --- /dev/null +++ b/sample_projects/15_ctx_state/uv.lock @@ -0,0 +1,1235 @@ +version = 1 +revision = 3 +requires-python = ">=3.11" + +[options] +exclude-newer = "2026-04-24T11:31:58.38087Z" +exclude-newer-span = "P7D" + +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "attrs" +version = "26.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, +] + +[[package]] +name = "beartype" +version = "0.22.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/94/1009e248bbfbab11397abca7193bea6626806be9a327d399810d523a07cb/beartype-0.22.9.tar.gz", hash = "sha256:8f82b54aa723a2848a56008d18875f91c1db02c32ef6a62319a002e3e25a975f", size = 1608866, upload-time = "2025-12-13T06:50:30.72Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/cc/18245721fa7747065ab478316c7fea7c74777d07f37ae60db2e84f8172e8/beartype-0.22.9-py3-none-any.whl", hash = "sha256:d16c9bbc61ea14637596c5f6fbff2ee99cbe3573e46a716401734ef50c3060c2", size = 1333658, upload-time = "2025-12-13T06:50:28.266Z" }, +] + +[[package]] +name = "beautifulsoup4" +version = "4.14.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "soupsieve" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c3/b0/1c6a16426d389813b48d95e26898aff79abbde42ad353958ad95cc8c9b21/beautifulsoup4-4.14.3.tar.gz", hash = "sha256:6292b1c5186d356bba669ef9f7f051757099565ad9ada5dd630bd9de5fa7fb86", size = 627737, upload-time = "2025-11-30T15:08:26.084Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-py3-none-any.whl", hash = "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb", size = 107721, upload-time = "2025-11-30T15:08:24.087Z" }, +] + +[[package]] +name = "bleach" +version = "6.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "webencodings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/07/18/3c8523962314be6bf4c8989c79ad9531c825210dd13a8669f6b84336e8bd/bleach-6.3.0.tar.gz", hash = "sha256:6f3b91b1c0a02bb9a78b5a454c92506aa0fdf197e1d5e114d2e00c6f64306d22", size = 203533, upload-time = "2025-10-27T17:57:39.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cd/3a/577b549de0cc09d95f11087ee63c739bba856cd3952697eec4c4bb91350a/bleach-6.3.0-py3-none-any.whl", hash = "sha256:fe10ec77c93ddf3d13a73b035abaac7a9f5e436513864ccdad516693213c65d6", size = 164437, upload-time = "2025-10-27T17:57:37.538Z" }, +] + +[package.optional-dependencies] +css = [ + { name = "tinycss2" }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, + { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, + { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, + { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, + { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, + { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, + { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, + { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, + { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, +] + +[[package]] +name = "click" +version = "8.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/63/f9e1ea081ce35720d8b92acde70daaedace594dc93b693c869e0d5910718/click-8.3.3.tar.gz", hash = "sha256:398329ad4837b2ff7cbe1dd166a4c0f8900c3ca3a218de04466f38f6497f18a2", size = 328061, upload-time = "2026-04-22T15:11:27.506Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl", hash = "sha256:a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613", size = 110502, upload-time = "2026-04-22T15:11:25.044Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "ctx-state-sample" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "netrun" }, +] + +[package.metadata] +requires-dist = [{ name = "netrun", editable = "../../netrun" }] + +[[package]] +name = "defusedxml" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520, upload-time = "2021-03-08T10:59:26.269Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" }, +] + +[[package]] +name = "diskcache" +version = "5.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3f/21/1c1ffc1a039ddcc459db43cc108658f32c57d271d7289a2794e401d0fdb6/diskcache-5.6.3.tar.gz", hash = "sha256:2c3a3fa2743d8535d832ec61c2054a1641f41775aa7c556758a109941e33e4fc", size = 67916, upload-time = "2023-08-31T06:12:00.316Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/27/4570e78fc0bf5ea0ca45eb1de3818a23787af9b390c0b0a0033a1b8236f9/diskcache-5.6.3-py3-none-any.whl", hash = "sha256:5e31b2d5fbad117cc363ebaf6b689474db18a1f6438bc82358b024abd4c2ca19", size = 45550, upload-time = "2023-08-31T06:11:58.822Z" }, +] + +[[package]] +name = "fastjsonschema" +version = "2.21.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/b5/23b216d9d985a956623b6bd12d4086b60f0059b27799f23016af04a74ea1/fastjsonschema-2.21.2.tar.gz", hash = "sha256:b1eb43748041c880796cd077f1a07c3d94e93ae84bba5ed36800a33554ae05de", size = 374130, upload-time = "2025-08-14T18:49:36.666Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/a8/20d0723294217e47de6d9e2e40fd4a9d2f7c4b6ef974babd482a59743694/fastjsonschema-2.21.2-py3-none-any.whl", hash = "sha256:1c797122d0a86c5cace2e54bf4e819c36223b552017172f32c5c024a6b77e463", size = 24024, upload-time = "2025-08-14T18:49:34.776Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "jsonschema" +version = "4.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, +] + +[[package]] +name = "jupyter-client" +version = "8.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jupyter-core" }, + { name = "python-dateutil" }, + { name = "pyzmq" }, + { name = "tornado" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/05/e4/ba649102a3bc3fbca54e7239fb924fd434c766f855693d86de0b1f2bec81/jupyter_client-8.8.0.tar.gz", hash = "sha256:d556811419a4f2d96c869af34e854e3f059b7cc2d6d01a9cd9c85c267691be3e", size = 348020, upload-time = "2026-01-08T13:55:47.938Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/0b/ceb7694d864abc0a047649aec263878acb9f792e1fec3e676f22dc9015e3/jupyter_client-8.8.0-py3-none-any.whl", hash = "sha256:f93a5b99c5e23a507b773d3a1136bd6e16c67883ccdbd9a829b0bbdb98cd7d7a", size = 107371, upload-time = "2026-01-08T13:55:45.562Z" }, +] + +[[package]] +name = "jupyter-core" +version = "5.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "platformdirs" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/49/9d1284d0dc65e2c757b74c6687b6d319b02f822ad039e5c512df9194d9dd/jupyter_core-5.9.1.tar.gz", hash = "sha256:4d09aaff303b9566c3ce657f580bd089ff5c91f5f89cf7d8846c3cdf465b5508", size = 89814, upload-time = "2025-10-16T19:19:18.444Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl", hash = "sha256:ebf87fdc6073d142e114c72c9e29a9d7ca03fad818c5d300ce2adc1fb0743407", size = 29032, upload-time = "2025-10-16T19:19:16.783Z" }, +] + +[[package]] +name = "jupyterlab-pygments" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/90/51/9187be60d989df97f5f0aba133fa54e7300f17616e065d1ada7d7646b6d6/jupyterlab_pygments-0.3.0.tar.gz", hash = "sha256:721aca4d9029252b11cfa9d185e5b5af4d54772bb8072f9b7036f4170054d35d", size = 512900, upload-time = "2023-11-23T09:26:37.44Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/dd/ead9d8ea85bf202d90cc513b533f9c363121c7792674f78e0d8a854b63b4/jupyterlab_pygments-0.3.0-py3-none-any.whl", hash = "sha256:841a89020971da1d8693f1a99997aefc5dc424bb1b251fd6322462a1b8842780", size = 15884, upload-time = "2023-11-23T09:26:34.325Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, + { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, + { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, + { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, + { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, + { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "mistune" +version = "3.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/55/d01f0c4b45ade6536c51170b9043db8b2ec6ddf4a35c7ea3f5f559ac935b/mistune-3.2.0.tar.gz", hash = "sha256:708487c8a8cdd99c9d90eb3ed4c3ed961246ff78ac82f03418f5183ab70e398a", size = 95467, upload-time = "2025-12-23T11:36:34.994Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/f7/4a5e785ec9fbd65146a27b6b70b6cdc161a66f2024e4b04ac06a67f5578b/mistune-3.2.0-py3-none-any.whl", hash = "sha256:febdc629a3c78616b94393c6580551e0e34cc289987ec6c35ed3f4be42d0eee1", size = 53598, upload-time = "2025-12-23T11:36:33.211Z" }, +] + +[[package]] +name = "nbclient" +version = "0.10.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jupyter-client" }, + { name = "jupyter-core" }, + { name = "nbformat" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/56/91/1c1d5a4b9a9ebba2b4e32b8c852c2975c872aec1fe42ab5e516b2cecd193/nbclient-0.10.4.tar.gz", hash = "sha256:1e54091b16e6da39e297b0ece3e10f6f29f4ac4e8ee515d29f8a7099bd6553c9", size = 62554, upload-time = "2025-12-23T07:45:46.369Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/a0/5b0c2f11142ed1dddec842457d3f65eaf71a0080894eb6f018755b319c3a/nbclient-0.10.4-py3-none-any.whl", hash = "sha256:9162df5a7373d70d606527300a95a975a47c137776cd942e52d9c7e29ff83440", size = 25465, upload-time = "2025-12-23T07:45:44.51Z" }, +] + +[[package]] +name = "nbconvert" +version = "7.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beautifulsoup4" }, + { name = "bleach", extra = ["css"] }, + { name = "defusedxml" }, + { name = "jinja2" }, + { name = "jupyter-core" }, + { name = "jupyterlab-pygments" }, + { name = "markupsafe" }, + { name = "mistune" }, + { name = "nbclient" }, + { name = "nbformat" }, + { name = "packaging" }, + { name = "pandocfilters" }, + { name = "pygments" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/01/b1/708e53fe2e429c103c6e6e159106bcf0357ac41aa4c28772bd8402339051/nbconvert-7.17.1.tar.gz", hash = "sha256:34d0d0a7e73ce3cbab6c5aae8f4f468797280b01fd8bd2ca746da8569eddd7d2", size = 865311, upload-time = "2026-04-08T00:44:14.914Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/f8/bb0a9d5f46819c821dc1f004aa2cc29b1d91453297dbf5ff20470f00f193/nbconvert-7.17.1-py3-none-any.whl", hash = "sha256:aa85c087b435e7bf1ffd03319f658e285f2b89eccab33bc1ba7025495ab3e7c8", size = 261927, upload-time = "2026-04-08T00:44:12.845Z" }, +] + +[[package]] +name = "nbformat" +version = "5.10.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fastjsonschema" }, + { name = "jsonschema" }, + { name = "jupyter-core" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/fd/91545e604bc3dad7dca9ed03284086039b294c6b3d75c0d2fa45f9e9caf3/nbformat-5.10.4.tar.gz", hash = "sha256:322168b14f937a5d11362988ecac2a4952d3d8e3a2cbeb2319584631226d5b3a", size = 142749, upload-time = "2024-04-04T11:20:37.371Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/82/0340caa499416c78e5d8f5f05947ae4bc3cba53c9f038ab6e9ed964e22f1/nbformat-5.10.4-py3-none-any.whl", hash = "sha256:3b48d6c8fbca4b299bf3982ea7db1af21580e4fec269ad087b9e81588891200b", size = 78454, upload-time = "2024-04-04T11:20:34.895Z" }, +] + +[[package]] +name = "nblite" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jinja2" }, + { name = "nbconvert" }, + { name = "nbformat" }, + { name = "notebookx-py" }, + { name = "pydantic" }, + { name = "pyyaml" }, + { name = "rich" }, + { name = "typer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/80/a5/e7a91b57e1dc17d0e546e95500388b957320588a563c20c053eb593b3353/nblite-1.2.1.tar.gz", hash = "sha256:0f5b129d93069f0f7502ddcc1f8d960af78eefb9d097b1b85bad088ec1618cbb", size = 325768, upload-time = "2026-03-15T10:50:07.001Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/4d/471d79351bab69bf96f12d3b689d430342857138bb45fb82cb08a19125d1/nblite-1.2.1-py3-none-any.whl", hash = "sha256:04083f525229f8d8445c08dc192286491ec63e730806c84faf226b9d33b35ca0", size = 105264, upload-time = "2026-03-15T10:50:05.235Z" }, +] + +[[package]] +name = "netrun" +version = "0.5.0" +source = { editable = "../../netrun" } +dependencies = [ + { name = "beartype" }, + { name = "diskcache" }, + { name = "nblite" }, + { name = "netrun-sim" }, + { name = "websockets" }, + { name = "xxhash" }, +] + +[package.metadata] +requires-dist = [ + { name = "beartype", specifier = ">=0.22.9" }, + { name = "cloudpickle", marker = "extra == 'serialization'", specifier = ">=3.0" }, + { name = "dill", marker = "extra == 'serialization'", specifier = ">=0.3" }, + { name = "diskcache", specifier = ">=5.6.3" }, + { name = "nblite", specifier = ">=1.1.1" }, + { name = "netrun-sim", editable = "../../netrun-sim/python" }, + { name = "pyinfra", marker = "extra == 'deploy'", specifier = ">=3.0" }, + { name = "websockets", specifier = ">=16.0" }, + { name = "xxhash", specifier = ">=3.6.0" }, +] +provides-extras = ["deploy", "serialization"] + +[package.metadata.requires-dev] +dev = [ + { name = "dev", editable = "../../netrun/src/dev" }, + { name = "jupyterlab", specifier = ">=4.5.1" }, + { name = "mypy", specifier = ">=1.19.1" }, + { name = "nblite", specifier = ">=1.1.5" }, + { name = "netrun-cli", editable = "../../netrun-cli" }, + { name = "pytest", specifier = ">=9.0.2" }, + { name = "pytest-asyncio", specifier = ">=1.3.0" }, + { name = "ruff", specifier = ">=0.8.0" }, +] + +[[package]] +name = "netrun-sim" +version = "0.2.0" +source = { editable = "../../netrun-sim/python" } +dependencies = [ + { name = "python-ulid" }, +] + +[package.metadata] +requires-dist = [{ name = "python-ulid", specifier = ">=1.0" }] + +[package.metadata.requires-dev] +dev = [ + { name = "git-cliff", specifier = ">=2.11.0" }, + { name = "jupyterlab", specifier = ">=4.3.8" }, + { name = "maturin", specifier = ">=1.10.2" }, + { name = "nblite", specifier = ">=1.1.1" }, + { name = "python-ulid", specifier = ">=1.1.0" }, + { name = "ruff", specifier = ">=0.14.11" }, + { name = "twine", specifier = ">=6.1.0" }, + { name = "typing-extensions", specifier = ">=4.13.2" }, +] + +[[package]] +name = "notebookx-py" +version = "0.1.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/8f/a052edfbddf51d4ec485bf05ec0b526fc9d4d476cbdea01b60a040b4dd4c/notebookx_py-0.1.8.tar.gz", hash = "sha256:cee26716a9436b7ee0771ec840c49d268068d1e7688b9a4d3da79c763cd07a11", size = 40139, upload-time = "2026-01-22T19:31:59.907Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/24/f1d34cf2dca8ad9c008345d6900460991535e9b708979a39e6c6df8580a6/notebookx_py-0.1.8-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:e30a0677f752acaa47bbee12f00b6a3a9caf48e1b10637699dc956c99dbcf17e", size = 762017, upload-time = "2026-01-22T19:31:48.983Z" }, + { url = "https://files.pythonhosted.org/packages/54/78/a43c065515b8a26a36e1b1ce96f15b166e4c5beb4d026317adc6db86074f/notebookx_py-0.1.8-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:6adfd281c4c4c4a6dea7ae9e9c5ed4e04359a1425a78d9e33acdd3914c579752", size = 732635, upload-time = "2026-01-22T19:31:51.027Z" }, + { url = "https://files.pythonhosted.org/packages/89/ed/7141c4baf3ef1d0ee80bd18b0c6c5b1ee45d6320c86c5c850fd2ab1d8d35/notebookx_py-0.1.8-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ae180b4ef7939b4567ca97e60c23acac79e5c17d66ccbca51f1b4faa72ed35a", size = 782241, upload-time = "2026-01-22T19:31:52.379Z" }, + { url = "https://files.pythonhosted.org/packages/ba/f3/b97946c846113f86537e202679d3322e06f5014d86e06fe061767d1cafd2/notebookx_py-0.1.8-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d34febdababaec0b5fca2355d3dbd6f69c3b0b596b26daec5ffa66c754bda94", size = 806947, upload-time = "2026-01-22T19:31:54.329Z" }, + { url = "https://files.pythonhosted.org/packages/07/0d/266611c951bea05711a2bf4791b035720a9daec1181d46589acc57d9f38c/notebookx_py-0.1.8-cp38-abi3-win_amd64.whl", hash = "sha256:fa4d013e4272d3ae714d5ca2206db9d263f85064ecb7380ee7bf722dee5d4625", size = 624603, upload-time = "2026-01-22T19:31:55.687Z" }, +] + +[[package]] +name = "packaging" +version = "26.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/df/de/0d2b39fb4af88a0258f3bac87dfcbb48e73fbdea4a2ed0e2213f9a4c2f9a/packaging-26.1.tar.gz", hash = "sha256:f042152b681c4bfac5cae2742a55e103d27ab2ec0f3d88037136b6bfe7c9c5de", size = 215519, upload-time = "2026-04-14T21:12:49.362Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/c2/920ef838e2f0028c8262f16101ec09ebd5969864e5a64c4c05fad0617c56/packaging-26.1-py3-none-any.whl", hash = "sha256:5d9c0669c6285e491e0ced2eee587eaf67b670d94a19e94e3984a481aba6802f", size = 95831, upload-time = "2026-04-14T21:12:47.56Z" }, +] + +[[package]] +name = "pandocfilters" +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/70/6f/3dd4940bbe001c06a65f88e36bad298bc7a0de5036115639926b0c5c0458/pandocfilters-1.5.1.tar.gz", hash = "sha256:002b4a555ee4ebc03f8b66307e287fa492e4a77b4ea14d3f934328297bb4939e", size = 8454, upload-time = "2024-01-18T20:08:13.726Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/af/4fbc8cab944db5d21b7e2a5b8e9211a03a79852b1157e2c102fcc61ac440/pandocfilters-1.5.1-py2.py3-none-any.whl", hash = "sha256:93be382804a9cdb0a7267585f157e5d1731bbe5545a85b268d6f5fe6232de2bc", size = 8663, upload-time = "2024-01-18T20:08:11.28Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.9.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9f/4a/0883b8e3802965322523f0b200ecf33d31f10991d0401162f4b23c698b42/platformdirs-4.9.6.tar.gz", hash = "sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a", size = 29400, upload-time = "2026-04-09T00:04:10.812Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/a6/a0a304dc33b49145b21f4808d763822111e67d1c3a32b524a1baf947b6e1/platformdirs-4.9.6-py3-none-any.whl", hash = "sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917", size = 21348, upload-time = "2026-04-09T00:04:09.463Z" }, +] + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + +[[package]] +name = "pydantic" +version = "2.13.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d9/e4/40d09941a2cebcb20609b86a559817d5b9291c49dd6f8c87e5feffbe703a/pydantic-2.13.3.tar.gz", hash = "sha256:af09e9d1d09f4e7fe37145c1f577e1d61ceb9a41924bf0094a36506285d0a84d", size = 844068, upload-time = "2026-04-20T14:46:43.632Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/0a/fd7d723f8f8153418fb40cf9c940e82004fce7e987026b08a68a36dd3fe7/pydantic-2.13.3-py3-none-any.whl", hash = "sha256:6db14ac8dfc9a1e57f87ea2c0de670c251240f43cb0c30a5130e9720dc612927", size = 471981, upload-time = "2026-04-20T14:46:41.402Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.46.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2a/ef/f7abb56c49382a246fd2ce9c799691e3c3e7175ec74b14d99e798bcddb1a/pydantic_core-2.46.3.tar.gz", hash = "sha256:41c178f65b8c29807239d47e6050262eb6bf84eb695e41101e62e38df4a5bc2c", size = 471412, upload-time = "2026-04-20T14:40:56.672Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/a2/1ba90a83e85a3f94c796b184f3efde9c72f2830dcda493eea8d59ba78e6d/pydantic_core-2.46.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ab124d49d0459b2373ecf54118a45c28a1e6d4192a533fbc915e70f556feb8e5", size = 2106740, upload-time = "2026-04-20T14:41:20.932Z" }, + { url = "https://files.pythonhosted.org/packages/b6/f6/99ae893c89a0b9d3daec9f95487aa676709aa83f67643b3f0abaf4ab628a/pydantic_core-2.46.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cca67d52a5c7a16aed2b3999e719c4bcf644074eac304a5d3d62dd70ae7d4b2c", size = 1948293, upload-time = "2026-04-20T14:43:42.115Z" }, + { url = "https://files.pythonhosted.org/packages/3e/b8/2e8e636dc9e3f16c2e16bf0849e24be82c5ee82c603c65fc0326666328fc/pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c024e08c0ba23e6fd68c771a521e9d6a792f2ebb0fa734296b36394dc30390e", size = 1973222, upload-time = "2026-04-20T14:41:57.841Z" }, + { url = "https://files.pythonhosted.org/packages/34/36/0e730beec4d83c5306f417afbd82ff237d9a21e83c5edf675f31ed84c1fe/pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6645ce7eec4928e29a1e3b3d5c946621d105d3e79f0c9cddf07c2a9770949287", size = 2053852, upload-time = "2026-04-20T14:40:43.077Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f0/3071131f47e39136a17814576e0fada9168569f7f8c0e6ac4d1ede6a4958/pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a712c7118e6c5ea96562f7b488435172abb94a3c53c22c9efc1412264a45cbbe", size = 2221134, upload-time = "2026-04-20T14:43:03.349Z" }, + { url = "https://files.pythonhosted.org/packages/2f/a9/a2dc023eec5aa4b02a467874bad32e2446957d2adcab14e107eab502e978/pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:69a868ef3ff206343579021c40faf3b1edc64b1cc508ff243a28b0a514ccb050", size = 2279785, upload-time = "2026-04-20T14:41:19.285Z" }, + { url = "https://files.pythonhosted.org/packages/0a/44/93f489d16fb63fbd41c670441536541f6e8cfa1e5a69f40bc9c5d30d8c90/pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc7e8c32db809aa0f6ea1d6869ebc8518a65d5150fdfad8bcae6a49ae32a22e2", size = 2089404, upload-time = "2026-04-20T14:43:10.108Z" }, + { url = "https://files.pythonhosted.org/packages/2a/78/8692e3aa72b2d004f7a5d937f1dfdc8552ba26caf0bec75f342c40f00dec/pydantic_core-2.46.3-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:3481bd1341dc85779ee506bc8e1196a277ace359d89d28588a9468c3ecbe63fa", size = 2114898, upload-time = "2026-04-20T14:44:51.475Z" }, + { url = "https://files.pythonhosted.org/packages/6a/62/e83133f2e7832532060175cebf1f13748f4c7e7e7165cdd1f611f174494b/pydantic_core-2.46.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8690eba565c6d68ffd3a8655525cbdd5246510b44a637ee2c6c03a7ebfe64d3c", size = 2157856, upload-time = "2026-04-20T14:43:46.64Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ec/6a500e3ad7718ee50583fae79c8651f5d37e3abce1fa9ae177ae65842c53/pydantic_core-2.46.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4de88889d7e88d50d40ee5b39d5dac0bcaef9ba91f7e536ac064e6b2834ecccf", size = 2180168, upload-time = "2026-04-20T14:42:00.302Z" }, + { url = "https://files.pythonhosted.org/packages/d8/53/8267811054b1aa7fc1dc7ded93812372ef79a839f5e23558136a6afbfde1/pydantic_core-2.46.3-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:e480080975c1ef7f780b8f99ed72337e7cc5efea2e518a20a692e8e7b278eb8b", size = 2322885, upload-time = "2026-04-20T14:41:05.253Z" }, + { url = "https://files.pythonhosted.org/packages/c8/c1/1c0acdb3aa0856ddc4ecc55214578f896f2de16f400cf51627eb3c26c1c4/pydantic_core-2.46.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:de3a5c376f8cd94da9a1b8fd3dd1c16c7a7b216ed31dc8ce9fd7a22bf13b836e", size = 2360328, upload-time = "2026-04-20T14:41:43.991Z" }, + { url = "https://files.pythonhosted.org/packages/f0/d0/ef39cd0f4a926814f360e71c1adeab48ad214d9727e4deb48eedfb5bce1a/pydantic_core-2.46.3-cp311-cp311-win32.whl", hash = "sha256:fc331a5314ffddd5385b9ee9d0d2fee0b13c27e0e02dad71b1ae5d6561f51eeb", size = 1979464, upload-time = "2026-04-20T14:43:12.215Z" }, + { url = "https://files.pythonhosted.org/packages/18/9c/f41951b0d858e343f1cf09398b2a7b3014013799744f2c4a8ad6a3eec4f2/pydantic_core-2.46.3-cp311-cp311-win_amd64.whl", hash = "sha256:b5b9c6cf08a8a5e502698f5e153056d12c34b8fb30317e0c5fd06f45162a6346", size = 2070837, upload-time = "2026-04-20T14:41:47.707Z" }, + { url = "https://files.pythonhosted.org/packages/9f/1e/264a17cd582f6ed50950d4d03dd5fefd84e570e238afe1cb3e25cf238769/pydantic_core-2.46.3-cp311-cp311-win_arm64.whl", hash = "sha256:5dfd51cf457482f04ec49491811a2b8fd5b843b64b11eecd2d7a1ee596ea78a6", size = 2053647, upload-time = "2026-04-20T14:42:27.535Z" }, + { url = "https://files.pythonhosted.org/packages/4b/cb/5b47425556ecc1f3fe18ed2a0083188aa46e1dd812b06e406475b3a5d536/pydantic_core-2.46.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b11b59b3eee90a80a36701ddb4576d9ae31f93f05cb9e277ceaa09e6bf074a67", size = 2101946, upload-time = "2026-04-20T14:40:52.581Z" }, + { url = "https://files.pythonhosted.org/packages/a1/4f/2fb62c2267cae99b815bbf4a7b9283812c88ca3153ef29f7707200f1d4e5/pydantic_core-2.46.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:af8653713055ea18a3abc1537fe2ebc42f5b0bbb768d1eb79fd74eb47c0ac089", size = 1951612, upload-time = "2026-04-20T14:42:42.996Z" }, + { url = "https://files.pythonhosted.org/packages/50/6e/b7348fd30d6556d132cddd5bd79f37f96f2601fe0608afac4f5fb01ec0b3/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:75a519dab6d63c514f3a81053e5266c549679e4aa88f6ec57f2b7b854aceb1b0", size = 1977027, upload-time = "2026-04-20T14:42:02.001Z" }, + { url = "https://files.pythonhosted.org/packages/82/11/31d60ee2b45540d3fb0b29302a393dbc01cd771c473f5b5147bcd353e593/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a6cd87cb1575b1ad05ba98894c5b5c96411ef678fa2f6ed2576607095b8d9789", size = 2063008, upload-time = "2026-04-20T14:44:17.952Z" }, + { url = "https://files.pythonhosted.org/packages/8a/db/3a9d1957181b59258f44a2300ab0f0be9d1e12d662a4f57bb31250455c52/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f80a55484b8d843c8ada81ebf70a682f3f00a3d40e378c06cf17ecb44d280d7d", size = 2233082, upload-time = "2026-04-20T14:40:57.934Z" }, + { url = "https://files.pythonhosted.org/packages/9c/e1/3277c38792aeb5cfb18c2f0c5785a221d9ff4e149abbe1184d53d5f72273/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3861f1731b90c50a3266316b9044f5c9b405eecb8e299b0a7120596334e4fe9c", size = 2304615, upload-time = "2026-04-20T14:42:12.584Z" }, + { url = "https://files.pythonhosted.org/packages/5e/d5/e3d9717c9eba10855325650afd2a9cba8e607321697f18953af9d562da2f/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb528e295ed31570ac3dcc9bfdd6e0150bc11ce6168ac87a8082055cf1a67395", size = 2094380, upload-time = "2026-04-20T14:43:05.522Z" }, + { url = "https://files.pythonhosted.org/packages/a1/20/abac35dedcbfd66c6f0b03e4e3564511771d6c9b7ede10a362d03e110d9b/pydantic_core-2.46.3-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:367508faa4973b992b271ba1494acaab36eb7e8739d1e47be5035fb1ea225396", size = 2135429, upload-time = "2026-04-20T14:41:55.549Z" }, + { url = "https://files.pythonhosted.org/packages/6c/a5/41bfd1df69afad71b5cf0535055bccc73022715ad362edbc124bc1e021d7/pydantic_core-2.46.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5ad3c826fe523e4becf4fe39baa44286cff85ef137c729a2c5e269afbfd0905d", size = 2174582, upload-time = "2026-04-20T14:41:45.96Z" }, + { url = "https://files.pythonhosted.org/packages/79/65/38d86ea056b29b2b10734eb23329b7a7672ca604df4f2b6e9c02d4ee22fe/pydantic_core-2.46.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ec638c5d194ef8af27db69f16c954a09797c0dc25015ad6123eb2c73a4d271ca", size = 2187533, upload-time = "2026-04-20T14:40:55.367Z" }, + { url = "https://files.pythonhosted.org/packages/b6/55/a1129141678a2026badc539ad1dee0a71d06f54c2f06a4bd68c030ac781b/pydantic_core-2.46.3-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:28ed528c45446062ee66edb1d33df5d88828ae167de76e773a3c7f64bd14e976", size = 2332985, upload-time = "2026-04-20T14:44:13.05Z" }, + { url = "https://files.pythonhosted.org/packages/d7/60/cb26f4077719f709e54819f4e8e1d43f4091f94e285eb6bd21e1190a7b7c/pydantic_core-2.46.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aed19d0c783886d5bd86d80ae5030006b45e28464218747dcf83dabfdd092c7b", size = 2373670, upload-time = "2026-04-20T14:41:53.421Z" }, + { url = "https://files.pythonhosted.org/packages/6b/7e/c3f21882bdf1d8d086876f81b5e296206c69c6082551d776895de7801fa0/pydantic_core-2.46.3-cp312-cp312-win32.whl", hash = "sha256:06d5d8820cbbdb4147578c1fe7ffcd5b83f34508cb9f9ab76e807be7db6ff0a4", size = 1966722, upload-time = "2026-04-20T14:44:30.588Z" }, + { url = "https://files.pythonhosted.org/packages/57/be/6b5e757b859013ebfbd7adba02f23b428f37c86dcbf78b5bb0b4ffd36e99/pydantic_core-2.46.3-cp312-cp312-win_amd64.whl", hash = "sha256:c3212fda0ee959c1dd04c60b601ec31097aaa893573a3a1abd0a47bcac2968c1", size = 2072970, upload-time = "2026-04-20T14:42:54.248Z" }, + { url = "https://files.pythonhosted.org/packages/bf/f8/a989b21cc75e9a32d24192ef700eea606521221a89faa40c919ce884f2b1/pydantic_core-2.46.3-cp312-cp312-win_arm64.whl", hash = "sha256:f1f8338dd7a7f31761f1f1a3c47503a9a3b34eea3c8b01fa6ee96408affb5e72", size = 2035963, upload-time = "2026-04-20T14:44:20.4Z" }, + { url = "https://files.pythonhosted.org/packages/9b/3c/9b5e8eb9821936d065439c3b0fb1490ffa64163bfe7e1595985a47896073/pydantic_core-2.46.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:12bc98de041458b80c86c56b24df1d23832f3e166cbaff011f25d187f5c62c37", size = 2102109, upload-time = "2026-04-20T14:41:24.219Z" }, + { url = "https://files.pythonhosted.org/packages/91/97/1c41d1f5a19f241d8069f1e249853bcce378cdb76eec8ab636d7bc426280/pydantic_core-2.46.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:85348b8f89d2c3508b65b16c3c33a4da22b8215138d8b996912bb1532868885f", size = 1951820, upload-time = "2026-04-20T14:42:14.236Z" }, + { url = "https://files.pythonhosted.org/packages/30/b4/d03a7ae14571bc2b6b3c7b122441154720619afe9a336fa3a95434df5e2f/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1105677a6df914b1fb71a81b96c8cce7726857e1717d86001f29be06a25ee6f8", size = 1977785, upload-time = "2026-04-20T14:42:31.648Z" }, + { url = "https://files.pythonhosted.org/packages/ae/0c/4086f808834b59e3c8f1aa26df8f4b6d998cdcf354a143d18ef41529d1fe/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:87082cd65669a33adeba5470769e9704c7cf026cc30afb9cc77fd865578ebaad", size = 2062761, upload-time = "2026-04-20T14:40:37.093Z" }, + { url = "https://files.pythonhosted.org/packages/fa/71/a649be5a5064c2df0db06e0a512c2281134ed2fcc981f52a657936a7527c/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60e5f66e12c4f5212d08522963380eaaeac5ebd795826cfd19b2dfb0c7a52b9c", size = 2232989, upload-time = "2026-04-20T14:42:59.254Z" }, + { url = "https://files.pythonhosted.org/packages/a2/84/7756e75763e810b3a710f4724441d1ecc5883b94aacb07ca71c5fb5cfb69/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b6cdf19bf84128d5e7c37e8a73a0c5c10d51103a650ac585d42dd6ae233f2b7f", size = 2303975, upload-time = "2026-04-20T14:41:32.287Z" }, + { url = "https://files.pythonhosted.org/packages/6c/35/68a762e0c1e31f35fa0dac733cbd9f5b118042853698de9509c8e5bf128b/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:031bb17f4885a43773c8c763089499f242aee2ea85cf17154168775dccdecf35", size = 2095325, upload-time = "2026-04-20T14:42:47.685Z" }, + { url = "https://files.pythonhosted.org/packages/77/bf/1bf8c9a8e91836c926eae5e3e51dce009bf495a60ca56060689d3df3f340/pydantic_core-2.46.3-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:bcf2a8b2982a6673693eae7348ef3d8cf3979c1d63b54fca7c397a635cc68687", size = 2133368, upload-time = "2026-04-20T14:41:22.766Z" }, + { url = "https://files.pythonhosted.org/packages/e5/50/87d818d6bab915984995157ceb2380f5aac4e563dddbed6b56f0ed057aba/pydantic_core-2.46.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28e8cf2f52d72ced402a137145923a762cbb5081e48b34312f7a0c8f55928ec3", size = 2173908, upload-time = "2026-04-20T14:42:52.044Z" }, + { url = "https://files.pythonhosted.org/packages/91/88/a311fb306d0bd6185db41fa14ae888fb81d0baf648a761ae760d30819d33/pydantic_core-2.46.3-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:17eaface65d9fc5abb940003020309c1bf7a211f5f608d7870297c367e6f9022", size = 2186422, upload-time = "2026-04-20T14:43:29.55Z" }, + { url = "https://files.pythonhosted.org/packages/8f/79/28fd0d81508525ab2054fef7c77a638c8b5b0afcbbaeee493cf7c3fef7e1/pydantic_core-2.46.3-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:93fd339f23408a07e98950a89644f92c54d8729719a40b30c0a30bb9ebc55d23", size = 2332709, upload-time = "2026-04-20T14:42:16.134Z" }, + { url = "https://files.pythonhosted.org/packages/b3/21/795bf5fe5c0f379308b8ef19c50dedab2e7711dbc8d0c2acf08f1c7daa05/pydantic_core-2.46.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:23cbdb3aaa74dfe0837975dbf69b469753bbde8eacace524519ffdb6b6e89eb7", size = 2372428, upload-time = "2026-04-20T14:41:10.974Z" }, + { url = "https://files.pythonhosted.org/packages/45/b3/ed14c659cbe7605e3ef063077680a64680aec81eb1a04763a05190d49b7f/pydantic_core-2.46.3-cp313-cp313-win32.whl", hash = "sha256:610eda2e3838f401105e6326ca304f5da1e15393ae25dacae5c5c63f2c275b13", size = 1965601, upload-time = "2026-04-20T14:41:42.128Z" }, + { url = "https://files.pythonhosted.org/packages/ef/bb/adb70d9a762ddd002d723fbf1bd492244d37da41e3af7b74ad212609027e/pydantic_core-2.46.3-cp313-cp313-win_amd64.whl", hash = "sha256:68cc7866ed863db34351294187f9b729964c371ba33e31c26f478471c52e1ed0", size = 2071517, upload-time = "2026-04-20T14:43:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/52/eb/66faefabebfe68bd7788339c9c9127231e680b11906368c67ce112fdb47f/pydantic_core-2.46.3-cp313-cp313-win_arm64.whl", hash = "sha256:f64b5537ac62b231572879cd08ec05600308636a5d63bcbdb15063a466977bec", size = 2035802, upload-time = "2026-04-20T14:43:38.507Z" }, + { url = "https://files.pythonhosted.org/packages/7f/db/a7bcb4940183fda36022cd18ba8dd12f2dff40740ec7b58ce7457befa416/pydantic_core-2.46.3-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:afa3aa644f74e290cdede48a7b0bee37d1c35e71b05105f6b340d484af536d9b", size = 2097614, upload-time = "2026-04-20T14:44:38.374Z" }, + { url = "https://files.pythonhosted.org/packages/24/35/e4066358a22e3e99519db370494c7528f5a2aa1367370e80e27e20283543/pydantic_core-2.46.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ced3310e51aa425f7f77da8bbbb5212616655bedbe82c70944320bc1dbe5e018", size = 1951896, upload-time = "2026-04-20T14:40:53.996Z" }, + { url = "https://files.pythonhosted.org/packages/87/92/37cf4049d1636996e4b888c05a501f40a43ff218983a551d57f9d5e14f0d/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e29908922ce9da1a30b4da490bd1d3d82c01dcfdf864d2a74aacee674d0bfa34", size = 1979314, upload-time = "2026-04-20T14:41:49.446Z" }, + { url = "https://files.pythonhosted.org/packages/d8/36/9ff4d676dfbdfb2d591cf43f3d90ded01e15b1404fd101180ed2d62a2fd3/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0c9ff69140423eea8ed2d5477df3ba037f671f5e897d206d921bc9fdc39613e7", size = 2056133, upload-time = "2026-04-20T14:42:23.574Z" }, + { url = "https://files.pythonhosted.org/packages/bc/f0/405b442a4d7ba855b06eec8b2bf9c617d43b8432d099dfdc7bf999293495/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b675ab0a0d5b1c8fdb81195dc5bcefea3f3c240871cdd7ff9a2de8aa50772eb2", size = 2228726, upload-time = "2026-04-20T14:44:22.816Z" }, + { url = "https://files.pythonhosted.org/packages/e7/f8/65cd92dd5a0bd89ba277a98ecbfaf6fc36bbd3300973c7a4b826d6ab1391/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0087084960f209a9a4af50ecd1fb063d9ad3658c07bb81a7a53f452dacbfb2ba", size = 2301214, upload-time = "2026-04-20T14:44:48.792Z" }, + { url = "https://files.pythonhosted.org/packages/fd/86/ef96a4c6e79e7a2d0410826a68fbc0eccc0fd44aa733be199d5fcac3bb87/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed42e6cc8e1b0e2b9b96e2276bad70ae625d10d6d524aed0c93de974ae029f9f", size = 2099927, upload-time = "2026-04-20T14:41:40.196Z" }, + { url = "https://files.pythonhosted.org/packages/6d/53/269caf30e0096e0a8a8f929d1982a27b3879872cca2d917d17c2f9fdf4fe/pydantic_core-2.46.3-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:f1771ce258afb3e4201e67d154edbbae712a76a6081079fe247c2f53c6322c22", size = 2128789, upload-time = "2026-04-20T14:41:15.868Z" }, + { url = "https://files.pythonhosted.org/packages/00/b0/1a6d9b6a587e118482910c244a1c5acf4d192604174132efd12bf0ac486f/pydantic_core-2.46.3-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a7610b6a5242a6c736d8ad47fd5fff87fcfe8f833b281b1c409c3d6835d9227f", size = 2173815, upload-time = "2026-04-20T14:44:25.152Z" }, + { url = "https://files.pythonhosted.org/packages/87/56/e7e00d4041a7e62b5a40815590114db3b535bf3ca0bf4dca9f16cef25246/pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:ff5e7783bcc5476e1db448bf268f11cb257b1c276d3e89f00b5727be86dd0127", size = 2181608, upload-time = "2026-04-20T14:41:28.933Z" }, + { url = "https://files.pythonhosted.org/packages/e8/22/4bd23c3d41f7c185d60808a1de83c76cf5aeabf792f6c636a55c3b1ec7f9/pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:9d2e32edcc143bc01e95300671915d9ca052d4f745aa0a49c48d4803f8a85f2c", size = 2326968, upload-time = "2026-04-20T14:42:03.962Z" }, + { url = "https://files.pythonhosted.org/packages/24/ac/66cd45129e3915e5ade3b292cb3bc7fd537f58f8f8dbdaba6170f7cabb74/pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:6e42d83d1c6b87fa56b521479cff237e626a292f3b31b6345c15a99121b454c1", size = 2369842, upload-time = "2026-04-20T14:41:35.52Z" }, + { url = "https://files.pythonhosted.org/packages/a2/51/dd4248abb84113615473aa20d5545b7c4cd73c8644003b5259686f93996c/pydantic_core-2.46.3-cp314-cp314-win32.whl", hash = "sha256:07bc6d2a28c3adb4f7c6ae46aa4f2d2929af127f587ed44057af50bf1ce0f505", size = 1959661, upload-time = "2026-04-20T14:41:00.042Z" }, + { url = "https://files.pythonhosted.org/packages/20/eb/59980e5f1ae54a3b86372bd9f0fa373ea2d402e8cdcd3459334430f91e91/pydantic_core-2.46.3-cp314-cp314-win_amd64.whl", hash = "sha256:8940562319bc621da30714617e6a7eaa6b98c84e8c685bcdc02d7ed5e7c7c44e", size = 2071686, upload-time = "2026-04-20T14:43:16.471Z" }, + { url = "https://files.pythonhosted.org/packages/8c/db/1cf77e5247047dfee34bc01fa9bca134854f528c8eb053e144298893d370/pydantic_core-2.46.3-cp314-cp314-win_arm64.whl", hash = "sha256:5dcbbcf4d22210ced8f837c96db941bdb078f419543472aca5d9a0bb7cddc7df", size = 2026907, upload-time = "2026-04-20T14:43:31.732Z" }, + { url = "https://files.pythonhosted.org/packages/57/c0/b3df9f6a543276eadba0a48487b082ca1f201745329d97dbfa287034a230/pydantic_core-2.46.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:d0fe3dce1e836e418f912c1ad91c73357d03e556a4d286f441bf34fed2dbeecf", size = 2095047, upload-time = "2026-04-20T14:42:37.982Z" }, + { url = "https://files.pythonhosted.org/packages/66/57/886a938073b97556c168fd99e1a7305bb363cd30a6d2c76086bf0587b32a/pydantic_core-2.46.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9ce92e58abc722dac1bf835a6798a60b294e48eb0e625ec9fd994b932ac5feee", size = 1934329, upload-time = "2026-04-20T14:43:49.655Z" }, + { url = "https://files.pythonhosted.org/packages/0b/7c/b42eaa5c34b13b07ecb51da21761297a9b8eb43044c864a035999998f328/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a03e6467f0f5ab796a486146d1b887b2dc5e5f9b3288898c1b1c3ad974e53e4a", size = 1974847, upload-time = "2026-04-20T14:42:10.737Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9b/92b42db6543e7de4f99ae977101a2967b63122d4b6cf7773812da2d7d5b5/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2798b6ba041b9d70acfb9071a2ea13c8456dd1e6a5555798e41ba7b0790e329c", size = 2041742, upload-time = "2026-04-20T14:40:44.262Z" }, + { url = "https://files.pythonhosted.org/packages/0f/19/46fbe1efabb5aa2834b43b9454e70f9a83ad9c338c1291e48bdc4fecf167/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9be3e221bdc6d69abf294dcf7aff6af19c31a5cdcc8f0aa3b14be29df4bd03b1", size = 2236235, upload-time = "2026-04-20T14:41:27.307Z" }, + { url = "https://files.pythonhosted.org/packages/77/da/b3f95bc009ad60ec53120f5d16c6faa8cabdbe8a20d83849a1f2b8728148/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f13936129ce841f2a5ddf6f126fea3c43cd128807b5a59588c37cf10178c2e64", size = 2282633, upload-time = "2026-04-20T14:44:33.271Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6e/401336117722e28f32fb8220df676769d28ebdf08f2f4469646d404c43a3/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28b5f2ef03416facccb1c6ef744c69793175fd27e44ef15669201601cf423acb", size = 2109679, upload-time = "2026-04-20T14:44:41.065Z" }, + { url = "https://files.pythonhosted.org/packages/fc/53/b289f9bc8756a32fe718c46f55afaeaf8d489ee18d1a1e7be1db73f42cc4/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:830d1247d77ad23852314f069e9d7ddafeec5f684baf9d7e7065ed46a049c4e6", size = 2108342, upload-time = "2026-04-20T14:42:50.144Z" }, + { url = "https://files.pythonhosted.org/packages/10/5b/8292fc7c1f9111f1b2b7c1b0dcf1179edcd014fc3ea4517499f50b829d71/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0793c90c1a3c74966e7975eaef3ed30ebdff3260a0f815a62a22adc17e4c01c", size = 2157208, upload-time = "2026-04-20T14:42:08.133Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9e/f80044e9ec07580f057a89fc131f78dda7a58751ddf52bbe05eaf31db50f/pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:d2d0aead851b66f5245ec0c4fb2612ef457f8bbafefdf65a2bf9d6bac6140f47", size = 2167237, upload-time = "2026-04-20T14:42:25.412Z" }, + { url = "https://files.pythonhosted.org/packages/f8/84/6781a1b037f3b96be9227edbd1101f6d3946746056231bf4ac48cdff1a8d/pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:2f40e4246676beb31c5ce77c38a55ca4e465c6b38d11ea1bd935420568e0b1ab", size = 2312540, upload-time = "2026-04-20T14:40:40.313Z" }, + { url = "https://files.pythonhosted.org/packages/3e/db/19c0839feeb728e7df03255581f198dfdf1c2aeb1e174a8420b63c5252e5/pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:cf489cf8986c543939aeee17a09c04d6ffb43bfef8ca16fcbcc5cfdcbed24dba", size = 2369556, upload-time = "2026-04-20T14:41:09.427Z" }, + { url = "https://files.pythonhosted.org/packages/e0/15/3228774cb7cd45f5f721ddf1b2242747f4eb834d0c491f0c02d606f09fed/pydantic_core-2.46.3-cp314-cp314t-win32.whl", hash = "sha256:ffe0883b56cfc05798bf994164d2b2ff03efe2d22022a2bb080f3b626176dd56", size = 1949756, upload-time = "2026-04-20T14:41:25.717Z" }, + { url = "https://files.pythonhosted.org/packages/b8/2a/c79cf53fd91e5a87e30d481809f52f9a60dd221e39de66455cf04deaad37/pydantic_core-2.46.3-cp314-cp314t-win_amd64.whl", hash = "sha256:706d9d0ce9cf4593d07270d8e9f53b161f90c57d315aeec4fb4fd7a8b10240d8", size = 2051305, upload-time = "2026-04-20T14:43:18.627Z" }, + { url = "https://files.pythonhosted.org/packages/0b/db/d8182a7f1d9343a032265aae186eb063fe26ca4c40f256b21e8da4498e89/pydantic_core-2.46.3-cp314-cp314t-win_arm64.whl", hash = "sha256:77706aeb41df6a76568434701e0917da10692da28cb69d5fb6919ce5fdb07374", size = 2026310, upload-time = "2026-04-20T14:41:01.778Z" }, + { url = "https://files.pythonhosted.org/packages/66/7f/03dbad45cd3aa9083fbc93c210ae8b005af67e4136a14186950a747c6874/pydantic_core-2.46.3-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:9715525891ed524a0a1eb6d053c74d4d4ad5017677fb00af0b7c2644a31bae46", size = 2105683, upload-time = "2026-04-20T14:42:19.779Z" }, + { url = "https://files.pythonhosted.org/packages/26/22/4dc186ac8ea6b257e9855031f51b62a9637beac4d68ac06bee02f046f836/pydantic_core-2.46.3-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:9d2f400712a99a013aff420ef1eb9be077f8189a36c1e3ef87660b4e1088a874", size = 1940052, upload-time = "2026-04-20T14:43:59.274Z" }, + { url = "https://files.pythonhosted.org/packages/0d/ca/d376391a5aff1f2e8188960d7873543608130a870961c2b6b5236627c116/pydantic_core-2.46.3-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd2aab0e2e9dc2daf36bd2686c982535d5e7b1d930a1344a7bb6e82baab42a76", size = 1988172, upload-time = "2026-04-20T14:41:17.469Z" }, + { url = "https://files.pythonhosted.org/packages/0e/6b/523b9f85c23788755d6ab949329de692a2e3a584bc6beb67fef5e035aa9d/pydantic_core-2.46.3-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e9d76736da5f362fabfeea6a69b13b7f2be405c6d6966f06b2f6bfff7e64531", size = 2128596, upload-time = "2026-04-20T14:40:41.707Z" }, + { url = "https://files.pythonhosted.org/packages/34/42/f426db557e8ab2791bc7562052299944a118655496fbff99914e564c0a94/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:b12dd51f1187c2eb489af8e20f880362db98e954b54ab792fa5d92e8bcc6b803", size = 2091877, upload-time = "2026-04-20T14:43:27.091Z" }, + { url = "https://files.pythonhosted.org/packages/5c/4f/86a832a9d14df58e663bfdf4627dc00d3317c2bd583c4fb23390b0f04b8e/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:f00a0961b125f1a47af7bcc17f00782e12f4cd056f83416006b30111d941dfa3", size = 1932428, upload-time = "2026-04-20T14:40:45.781Z" }, + { url = "https://files.pythonhosted.org/packages/11/1a/fe857968954d93fb78e0d4b6df5c988c74c4aaa67181c60be7cfe327c0ca/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57697d7c056aca4bbb680200f96563e841a6386ac1129370a0102592f4dddff5", size = 1997550, upload-time = "2026-04-20T14:44:02.425Z" }, + { url = "https://files.pythonhosted.org/packages/17/eb/9d89ad2d9b0ba8cd65393d434471621b98912abb10fbe1df08e480ba57b5/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd35aa21299def8db7ef4fe5c4ff862941a9a158ca7b63d61e66fe67d30416b4", size = 2137657, upload-time = "2026-04-20T14:42:45.149Z" }, + { url = "https://files.pythonhosted.org/packages/1f/da/99d40830684f81dec901cac521b5b91c095394cc1084b9433393cde1c2df/pydantic_core-2.46.3-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:13afdd885f3d71280cf286b13b310ee0f7ccfefd1dbbb661514a474b726e2f25", size = 2107973, upload-time = "2026-04-20T14:42:06.175Z" }, + { url = "https://files.pythonhosted.org/packages/99/a5/87024121818d75bbb2a98ddbaf638e40e7a18b5e0f5492c9ca4b1b316107/pydantic_core-2.46.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f91c0aff3e3ee0928edd1232c57f643a7a003e6edf1860bc3afcdc749cb513f3", size = 1947191, upload-time = "2026-04-20T14:43:14.319Z" }, + { url = "https://files.pythonhosted.org/packages/60/62/0c1acfe10945b83a6a59d19fbaa92f48825381509e5701b855c08f13db76/pydantic_core-2.46.3-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6529d1d128321a58d30afcc97b49e98836542f68dd41b33c2e972bb9e5290536", size = 2123791, upload-time = "2026-04-20T14:43:22.766Z" }, + { url = "https://files.pythonhosted.org/packages/75/3e/3b2393b4c8f44285561dc30b00cf307a56a2eff7c483a824db3b8221ca51/pydantic_core-2.46.3-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:975c267cff4f7e7272eacbe50f6cc03ca9a3da4c4fbd66fffd89c94c1e311aa1", size = 2153197, upload-time = "2026-04-20T14:44:27.932Z" }, + { url = "https://files.pythonhosted.org/packages/ba/75/5af02fb35505051eee727c061f2881c555ab4f8ddb2d42da715a42c9731b/pydantic_core-2.46.3-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:2b8e4f2bbdf71415c544b4b1138b8060db7b6611bc927e8064c769f64bed651c", size = 2181073, upload-time = "2026-04-20T14:43:20.729Z" }, + { url = "https://files.pythonhosted.org/packages/10/92/7e0e1bd9ca3c68305db037560ca2876f89b2647deb2f8b6319005de37505/pydantic_core-2.46.3-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:e61ea8e9fff9606d09178f577ff8ccdd7206ff73d6552bcec18e1033c4254b85", size = 2315886, upload-time = "2026-04-20T14:44:04.826Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d8/101655f27eaf3e44558ead736b2795d12500598beed4683f279396fa186e/pydantic_core-2.46.3-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b504bda01bafc69b6d3c7a0c7f039dcf60f47fab70e06fe23f57b5c75bdc82b8", size = 2360528, upload-time = "2026-04-20T14:40:47.431Z" }, + { url = "https://files.pythonhosted.org/packages/07/0f/1c34a74c8d07136f0d729ffe5e1fdab04fbdaa7684f61a92f92511a84a15/pydantic_core-2.46.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:b00b76f7142fc60c762ce579bd29c8fa44aaa56592dd3c54fab3928d0d4ca6ff", size = 2184144, upload-time = "2026-04-20T14:42:57Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[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, upload-time = "2024-03-01T18:36:20.211Z" } +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, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "python-ulid" +version = "3.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/40/7e/0d6c82b5ccc71e7c833aed43d9e8468e1f2ff0be1b3f657a6fcafbb8433d/python_ulid-3.1.0.tar.gz", hash = "sha256:ff0410a598bc5f6b01b602851a3296ede6f91389f913a5d5f8c496003836f636", size = 93175, upload-time = "2025-08-18T16:09:26.305Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6c/a0/4ed6632b70a52de845df056654162acdebaf97c20e3212c559ac43e7216e/python_ulid-3.1.0-py3-none-any.whl", hash = "sha256:e2cdc979c8c877029b4b7a38a6fba3bc4578e4f109a308419ff4d3ccf0a46619", size = 11577, upload-time = "2025-08-18T16:09:25.047Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "pyzmq" +version = "27.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "implementation_name == 'pypy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/0b/3c9baedbdf613ecaa7aa07027780b8867f57b6293b6ee50de316c9f3222b/pyzmq-27.1.0.tar.gz", hash = "sha256:ac0765e3d44455adb6ddbf4417dcce460fc40a05978c08efdf2948072f6db540", size = 281750, upload-time = "2025-09-08T23:10:18.157Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/5d/305323ba86b284e6fcb0d842d6adaa2999035f70f8c38a9b6d21ad28c3d4/pyzmq-27.1.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:226b091818d461a3bef763805e75685e478ac17e9008f49fce2d3e52b3d58b86", size = 1333328, upload-time = "2025-09-08T23:07:45.946Z" }, + { url = "https://files.pythonhosted.org/packages/bd/a0/fc7e78a23748ad5443ac3275943457e8452da67fda347e05260261108cbc/pyzmq-27.1.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:0790a0161c281ca9723f804871b4027f2e8b5a528d357c8952d08cd1a9c15581", size = 908803, upload-time = "2025-09-08T23:07:47.551Z" }, + { url = "https://files.pythonhosted.org/packages/7e/22/37d15eb05f3bdfa4abea6f6d96eb3bb58585fbd3e4e0ded4e743bc650c97/pyzmq-27.1.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c895a6f35476b0c3a54e3eb6ccf41bf3018de937016e6e18748317f25d4e925f", size = 668836, upload-time = "2025-09-08T23:07:49.436Z" }, + { url = "https://files.pythonhosted.org/packages/b1/c4/2a6fe5111a01005fc7af3878259ce17684fabb8852815eda6225620f3c59/pyzmq-27.1.0-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bbf8d3630bf96550b3be8e1fc0fea5cbdc8d5466c1192887bd94869da17a63e", size = 857038, upload-time = "2025-09-08T23:07:51.234Z" }, + { url = "https://files.pythonhosted.org/packages/cb/eb/bfdcb41d0db9cd233d6fb22dc131583774135505ada800ebf14dfb0a7c40/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:15c8bd0fe0dabf808e2d7a681398c4e5ded70a551ab47482067a572c054c8e2e", size = 1657531, upload-time = "2025-09-08T23:07:52.795Z" }, + { url = "https://files.pythonhosted.org/packages/ab/21/e3180ca269ed4a0de5c34417dfe71a8ae80421198be83ee619a8a485b0c7/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:bafcb3dd171b4ae9f19ee6380dfc71ce0390fefaf26b504c0e5f628d7c8c54f2", size = 2034786, upload-time = "2025-09-08T23:07:55.047Z" }, + { url = "https://files.pythonhosted.org/packages/3b/b1/5e21d0b517434b7f33588ff76c177c5a167858cc38ef740608898cd329f2/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e829529fcaa09937189178115c49c504e69289abd39967cd8a4c215761373394", size = 1894220, upload-time = "2025-09-08T23:07:57.172Z" }, + { url = "https://files.pythonhosted.org/packages/03/f2/44913a6ff6941905efc24a1acf3d3cb6146b636c546c7406c38c49c403d4/pyzmq-27.1.0-cp311-cp311-win32.whl", hash = "sha256:6df079c47d5902af6db298ec92151db82ecb557af663098b92f2508c398bb54f", size = 567155, upload-time = "2025-09-08T23:07:59.05Z" }, + { url = "https://files.pythonhosted.org/packages/23/6d/d8d92a0eb270a925c9b4dd039c0b4dc10abc2fcbc48331788824ef113935/pyzmq-27.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:190cbf120fbc0fc4957b56866830def56628934a9d112aec0e2507aa6a032b97", size = 633428, upload-time = "2025-09-08T23:08:00.663Z" }, + { url = "https://files.pythonhosted.org/packages/ae/14/01afebc96c5abbbd713ecfc7469cfb1bc801c819a74ed5c9fad9a48801cb/pyzmq-27.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:eca6b47df11a132d1745eb3b5b5e557a7dae2c303277aa0e69c6ba91b8736e07", size = 559497, upload-time = "2025-09-08T23:08:02.15Z" }, + { url = "https://files.pythonhosted.org/packages/92/e7/038aab64a946d535901103da16b953c8c9cc9c961dadcbf3609ed6428d23/pyzmq-27.1.0-cp312-abi3-macosx_10_15_universal2.whl", hash = "sha256:452631b640340c928fa343801b0d07eb0c3789a5ffa843f6e1a9cee0ba4eb4fc", size = 1306279, upload-time = "2025-09-08T23:08:03.807Z" }, + { url = "https://files.pythonhosted.org/packages/e8/5e/c3c49fdd0f535ef45eefcc16934648e9e59dace4a37ee88fc53f6cd8e641/pyzmq-27.1.0-cp312-abi3-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1c179799b118e554b66da67d88ed66cd37a169f1f23b5d9f0a231b4e8d44a113", size = 895645, upload-time = "2025-09-08T23:08:05.301Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e5/b0b2504cb4e903a74dcf1ebae157f9e20ebb6ea76095f6cfffea28c42ecd/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3837439b7f99e60312f0c926a6ad437b067356dc2bc2ec96eb395fd0fe804233", size = 652574, upload-time = "2025-09-08T23:08:06.828Z" }, + { url = "https://files.pythonhosted.org/packages/f8/9b/c108cdb55560eaf253f0cbdb61b29971e9fb34d9c3499b0e96e4e60ed8a5/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43ad9a73e3da1fab5b0e7e13402f0b2fb934ae1c876c51d0afff0e7c052eca31", size = 840995, upload-time = "2025-09-08T23:08:08.396Z" }, + { url = "https://files.pythonhosted.org/packages/c2/bb/b79798ca177b9eb0825b4c9998c6af8cd2a7f15a6a1a4272c1d1a21d382f/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0de3028d69d4cdc475bfe47a6128eb38d8bc0e8f4d69646adfbcd840facbac28", size = 1642070, upload-time = "2025-09-08T23:08:09.989Z" }, + { url = "https://files.pythonhosted.org/packages/9c/80/2df2e7977c4ede24c79ae39dcef3899bfc5f34d1ca7a5b24f182c9b7a9ca/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_i686.whl", hash = "sha256:cf44a7763aea9298c0aa7dbf859f87ed7012de8bda0f3977b6fb1d96745df856", size = 2021121, upload-time = "2025-09-08T23:08:11.907Z" }, + { url = "https://files.pythonhosted.org/packages/46/bd/2d45ad24f5f5ae7e8d01525eb76786fa7557136555cac7d929880519e33a/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f30f395a9e6fbca195400ce833c731e7b64c3919aa481af4d88c3759e0cb7496", size = 1878550, upload-time = "2025-09-08T23:08:13.513Z" }, + { url = "https://files.pythonhosted.org/packages/e6/2f/104c0a3c778d7c2ab8190e9db4f62f0b6957b53c9d87db77c284b69f33ea/pyzmq-27.1.0-cp312-abi3-win32.whl", hash = "sha256:250e5436a4ba13885494412b3da5d518cd0d3a278a1ae640e113c073a5f88edd", size = 559184, upload-time = "2025-09-08T23:08:15.163Z" }, + { url = "https://files.pythonhosted.org/packages/fc/7f/a21b20d577e4100c6a41795842028235998a643b1ad406a6d4163ea8f53e/pyzmq-27.1.0-cp312-abi3-win_amd64.whl", hash = "sha256:9ce490cf1d2ca2ad84733aa1d69ce6855372cb5ce9223802450c9b2a7cba0ccf", size = 619480, upload-time = "2025-09-08T23:08:17.192Z" }, + { url = "https://files.pythonhosted.org/packages/78/c2/c012beae5f76b72f007a9e91ee9401cb88c51d0f83c6257a03e785c81cc2/pyzmq-27.1.0-cp312-abi3-win_arm64.whl", hash = "sha256:75a2f36223f0d535a0c919e23615fc85a1e23b71f40c7eb43d7b1dedb4d8f15f", size = 552993, upload-time = "2025-09-08T23:08:18.926Z" }, + { url = "https://files.pythonhosted.org/packages/60/cb/84a13459c51da6cec1b7b1dc1a47e6db6da50b77ad7fd9c145842750a011/pyzmq-27.1.0-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:93ad4b0855a664229559e45c8d23797ceac03183c7b6f5b4428152a6b06684a5", size = 1122436, upload-time = "2025-09-08T23:08:20.801Z" }, + { url = "https://files.pythonhosted.org/packages/dc/b6/94414759a69a26c3dd674570a81813c46a078767d931a6c70ad29fc585cb/pyzmq-27.1.0-cp313-cp313-android_24_x86_64.whl", hash = "sha256:fbb4f2400bfda24f12f009cba62ad5734148569ff4949b1b6ec3b519444342e6", size = 1156301, upload-time = "2025-09-08T23:08:22.47Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ad/15906493fd40c316377fd8a8f6b1f93104f97a752667763c9b9c1b71d42d/pyzmq-27.1.0-cp313-cp313t-macosx_10_15_universal2.whl", hash = "sha256:e343d067f7b151cfe4eb3bb796a7752c9d369eed007b91231e817071d2c2fec7", size = 1341197, upload-time = "2025-09-08T23:08:24.286Z" }, + { url = "https://files.pythonhosted.org/packages/14/1d/d343f3ce13db53a54cb8946594e567410b2125394dafcc0268d8dda027e0/pyzmq-27.1.0-cp313-cp313t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:08363b2011dec81c354d694bdecaef4770e0ae96b9afea70b3f47b973655cc05", size = 897275, upload-time = "2025-09-08T23:08:26.063Z" }, + { url = "https://files.pythonhosted.org/packages/69/2d/d83dd6d7ca929a2fc67d2c3005415cdf322af7751d773524809f9e585129/pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d54530c8c8b5b8ddb3318f481297441af102517602b569146185fa10b63f4fa9", size = 660469, upload-time = "2025-09-08T23:08:27.623Z" }, + { url = "https://files.pythonhosted.org/packages/3e/cd/9822a7af117f4bc0f1952dbe9ef8358eb50a24928efd5edf54210b850259/pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f3afa12c392f0a44a2414056d730eebc33ec0926aae92b5ad5cf26ebb6cc128", size = 847961, upload-time = "2025-09-08T23:08:29.672Z" }, + { url = "https://files.pythonhosted.org/packages/9a/12/f003e824a19ed73be15542f172fd0ec4ad0b60cf37436652c93b9df7c585/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c65047adafe573ff023b3187bb93faa583151627bc9c51fc4fb2c561ed689d39", size = 1650282, upload-time = "2025-09-08T23:08:31.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4a/e82d788ed58e9a23995cee70dbc20c9aded3d13a92d30d57ec2291f1e8a3/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:90e6e9441c946a8b0a667356f7078d96411391a3b8f80980315455574177ec97", size = 2024468, upload-time = "2025-09-08T23:08:33.543Z" }, + { url = "https://files.pythonhosted.org/packages/d9/94/2da0a60841f757481e402b34bf4c8bf57fa54a5466b965de791b1e6f747d/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:add071b2d25f84e8189aaf0882d39a285b42fa3853016ebab234a5e78c7a43db", size = 1885394, upload-time = "2025-09-08T23:08:35.51Z" }, + { url = "https://files.pythonhosted.org/packages/4f/6f/55c10e2e49ad52d080dc24e37adb215e5b0d64990b57598abc2e3f01725b/pyzmq-27.1.0-cp313-cp313t-win32.whl", hash = "sha256:7ccc0700cfdf7bd487bea8d850ec38f204478681ea02a582a8da8171b7f90a1c", size = 574964, upload-time = "2025-09-08T23:08:37.178Z" }, + { url = "https://files.pythonhosted.org/packages/87/4d/2534970ba63dd7c522d8ca80fb92777f362c0f321900667c615e2067cb29/pyzmq-27.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:8085a9fba668216b9b4323be338ee5437a235fe275b9d1610e422ccc279733e2", size = 641029, upload-time = "2025-09-08T23:08:40.595Z" }, + { url = "https://files.pythonhosted.org/packages/f6/fa/f8aea7a28b0641f31d40dea42d7ef003fded31e184ef47db696bc74cd610/pyzmq-27.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:6bb54ca21bcfe361e445256c15eedf083f153811c37be87e0514934d6913061e", size = 561541, upload-time = "2025-09-08T23:08:42.668Z" }, + { url = "https://files.pythonhosted.org/packages/87/45/19efbb3000956e82d0331bafca5d9ac19ea2857722fa2caacefb6042f39d/pyzmq-27.1.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ce980af330231615756acd5154f29813d553ea555485ae712c491cd483df6b7a", size = 1341197, upload-time = "2025-09-08T23:08:44.973Z" }, + { url = "https://files.pythonhosted.org/packages/48/43/d72ccdbf0d73d1343936296665826350cb1e825f92f2db9db3e61c2162a2/pyzmq-27.1.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1779be8c549e54a1c38f805e56d2a2e5c009d26de10921d7d51cfd1c8d4632ea", size = 897175, upload-time = "2025-09-08T23:08:46.601Z" }, + { url = "https://files.pythonhosted.org/packages/2f/2e/a483f73a10b65a9ef0161e817321d39a770b2acf8bcf3004a28d90d14a94/pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7200bb0f03345515df50d99d3db206a0a6bee1955fbb8c453c76f5bf0e08fb96", size = 660427, upload-time = "2025-09-08T23:08:48.187Z" }, + { url = "https://files.pythonhosted.org/packages/f5/d2/5f36552c2d3e5685abe60dfa56f91169f7a2d99bbaf67c5271022ab40863/pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01c0e07d558b06a60773744ea6251f769cd79a41a97d11b8bf4ab8f034b0424d", size = 847929, upload-time = "2025-09-08T23:08:49.76Z" }, + { url = "https://files.pythonhosted.org/packages/c4/2a/404b331f2b7bf3198e9945f75c4c521f0c6a3a23b51f7a4a401b94a13833/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:80d834abee71f65253c91540445d37c4c561e293ba6e741b992f20a105d69146", size = 1650193, upload-time = "2025-09-08T23:08:51.7Z" }, + { url = "https://files.pythonhosted.org/packages/1c/0b/f4107e33f62a5acf60e3ded67ed33d79b4ce18de432625ce2fc5093d6388/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:544b4e3b7198dde4a62b8ff6685e9802a9a1ebf47e77478a5eb88eca2a82f2fd", size = 2024388, upload-time = "2025-09-08T23:08:53.393Z" }, + { url = "https://files.pythonhosted.org/packages/0d/01/add31fe76512642fd6e40e3a3bd21f4b47e242c8ba33efb6809e37076d9b/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cedc4c68178e59a4046f97eca31b148ddcf51e88677de1ef4e78cf06c5376c9a", size = 1885316, upload-time = "2025-09-08T23:08:55.702Z" }, + { url = "https://files.pythonhosted.org/packages/c4/59/a5f38970f9bf07cee96128de79590bb354917914a9be11272cfc7ff26af0/pyzmq-27.1.0-cp314-cp314t-win32.whl", hash = "sha256:1f0b2a577fd770aa6f053211a55d1c47901f4d537389a034c690291485e5fe92", size = 587472, upload-time = "2025-09-08T23:08:58.18Z" }, + { url = "https://files.pythonhosted.org/packages/70/d8/78b1bad170f93fcf5e3536e70e8fadac55030002275c9a29e8f5719185de/pyzmq-27.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:19c9468ae0437f8074af379e986c5d3d7d7bfe033506af442e8c879732bedbe0", size = 661401, upload-time = "2025-09-08T23:08:59.802Z" }, + { url = "https://files.pythonhosted.org/packages/81/d6/4bfbb40c9a0b42fc53c7cf442f6385db70b40f74a783130c5d0a5aa62228/pyzmq-27.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dc5dbf68a7857b59473f7df42650c621d7e8923fb03fa74a526890f4d33cc4d7", size = 575170, upload-time = "2025-09-08T23:09:01.418Z" }, + { url = "https://files.pythonhosted.org/packages/4c/c6/c4dcdecdbaa70969ee1fdced6d7b8f60cfabe64d25361f27ac4665a70620/pyzmq-27.1.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:18770c8d3563715387139060d37859c02ce40718d1faf299abddcdcc6a649066", size = 836265, upload-time = "2025-09-08T23:09:49.376Z" }, + { url = "https://files.pythonhosted.org/packages/3e/79/f38c92eeaeb03a2ccc2ba9866f0439593bb08c5e3b714ac1d553e5c96e25/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:ac25465d42f92e990f8d8b0546b01c391ad431c3bf447683fdc40565941d0604", size = 800208, upload-time = "2025-09-08T23:09:51.073Z" }, + { url = "https://files.pythonhosted.org/packages/49/0e/3f0d0d335c6b3abb9b7b723776d0b21fa7f3a6c819a0db6097059aada160/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53b40f8ae006f2734ee7608d59ed661419f087521edbfc2149c3932e9c14808c", size = 567747, upload-time = "2025-09-08T23:09:52.698Z" }, + { url = "https://files.pythonhosted.org/packages/a1/cf/f2b3784d536250ffd4be70e049f3b60981235d70c6e8ce7e3ef21e1adb25/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f605d884e7c8be8fe1aa94e0a783bf3f591b84c24e4bc4f3e7564c82ac25e271", size = 747371, upload-time = "2025-09-08T23:09:54.563Z" }, + { url = "https://files.pythonhosted.org/packages/01/1b/5dbe84eefc86f48473947e2f41711aded97eecef1231f4558f1f02713c12/pyzmq-27.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c9f7f6e13dff2e44a6afeaf2cf54cee5929ad64afaf4d40b50f93c58fc687355", size = 544862, upload-time = "2025-09-08T23:09:56.509Z" }, +] + +[[package]] +name = "referencing" +version = "0.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, +] + +[[package]] +name = "rich" +version = "15.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, +] + +[[package]] +name = "rpds-py" +version = "0.30.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/6e/f964e88b3d2abee2a82c1ac8366da848fce1c6d834dc2132c3fda3970290/rpds_py-0.30.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425", size = 370157, upload-time = "2025-11-30T20:21:53.789Z" }, + { url = "https://files.pythonhosted.org/packages/94/ba/24e5ebb7c1c82e74c4e4f33b2112a5573ddc703915b13a073737b59b86e0/rpds_py-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d", size = 359676, upload-time = "2025-11-30T20:21:55.475Z" }, + { url = "https://files.pythonhosted.org/packages/84/86/04dbba1b087227747d64d80c3b74df946b986c57af0a9f0c98726d4d7a3b/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4", size = 389938, upload-time = "2025-11-30T20:21:57.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/bb/1463f0b1722b7f45431bdd468301991d1328b16cffe0b1c2918eba2c4eee/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f", size = 402932, upload-time = "2025-11-30T20:21:58.47Z" }, + { url = "https://files.pythonhosted.org/packages/99/ee/2520700a5c1f2d76631f948b0736cdf9b0acb25abd0ca8e889b5c62ac2e3/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4", size = 525830, upload-time = "2025-11-30T20:21:59.699Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ad/bd0331f740f5705cc555a5e17fdf334671262160270962e69a2bdef3bf76/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97", size = 412033, upload-time = "2025-11-30T20:22:00.991Z" }, + { url = "https://files.pythonhosted.org/packages/f8/1e/372195d326549bb51f0ba0f2ecb9874579906b97e08880e7a65c3bef1a99/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89", size = 390828, upload-time = "2025-11-30T20:22:02.723Z" }, + { url = "https://files.pythonhosted.org/packages/ab/2b/d88bb33294e3e0c76bc8f351a3721212713629ffca1700fa94979cb3eae8/rpds_py-0.30.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d", size = 404683, upload-time = "2025-11-30T20:22:04.367Z" }, + { url = "https://files.pythonhosted.org/packages/50/32/c759a8d42bcb5289c1fac697cd92f6fe01a018dd937e62ae77e0e7f15702/rpds_py-0.30.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038", size = 421583, upload-time = "2025-11-30T20:22:05.814Z" }, + { url = "https://files.pythonhosted.org/packages/2b/81/e729761dbd55ddf5d84ec4ff1f47857f4374b0f19bdabfcf929164da3e24/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7", size = 572496, upload-time = "2025-11-30T20:22:07.713Z" }, + { url = "https://files.pythonhosted.org/packages/14/f6/69066a924c3557c9c30baa6ec3a0aa07526305684c6f86c696b08860726c/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed", size = 598669, upload-time = "2025-11-30T20:22:09.312Z" }, + { url = "https://files.pythonhosted.org/packages/5f/48/905896b1eb8a05630d20333d1d8ffd162394127b74ce0b0784ae04498d32/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85", size = 561011, upload-time = "2025-11-30T20:22:11.309Z" }, + { url = "https://files.pythonhosted.org/packages/22/16/cd3027c7e279d22e5eb431dd3c0fbc677bed58797fe7581e148f3f68818b/rpds_py-0.30.0-cp311-cp311-win32.whl", hash = "sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c", size = 221406, upload-time = "2025-11-30T20:22:13.101Z" }, + { url = "https://files.pythonhosted.org/packages/fa/5b/e7b7aa136f28462b344e652ee010d4de26ee9fd16f1bfd5811f5153ccf89/rpds_py-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825", size = 236024, upload-time = "2025-11-30T20:22:14.853Z" }, + { url = "https://files.pythonhosted.org/packages/14/a6/364bba985e4c13658edb156640608f2c9e1d3ea3c81b27aa9d889fff0e31/rpds_py-0.30.0-cp311-cp311-win_arm64.whl", hash = "sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229", size = 229069, upload-time = "2025-11-30T20:22:16.577Z" }, + { url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086, upload-time = "2025-11-30T20:22:17.93Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053, upload-time = "2025-11-30T20:22:19.297Z" }, + { url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763, upload-time = "2025-11-30T20:22:21.661Z" }, + { url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951, upload-time = "2025-11-30T20:22:23.408Z" }, + { url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622, upload-time = "2025-11-30T20:22:25.16Z" }, + { url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492, upload-time = "2025-11-30T20:22:26.505Z" }, + { url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080, upload-time = "2025-11-30T20:22:27.934Z" }, + { url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680, upload-time = "2025-11-30T20:22:29.341Z" }, + { url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589, upload-time = "2025-11-30T20:22:31.469Z" }, + { url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289, upload-time = "2025-11-30T20:22:32.997Z" }, + { url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737, upload-time = "2025-11-30T20:22:34.419Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120, upload-time = "2025-11-30T20:22:35.903Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782, upload-time = "2025-11-30T20:22:37.271Z" }, + { url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463, upload-time = "2025-11-30T20:22:39.021Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868, upload-time = "2025-11-30T20:22:40.493Z" }, + { url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887, upload-time = "2025-11-30T20:22:41.812Z" }, + { url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904, upload-time = "2025-11-30T20:22:43.479Z" }, + { url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945, upload-time = "2025-11-30T20:22:44.819Z" }, + { url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783, upload-time = "2025-11-30T20:22:46.103Z" }, + { url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021, upload-time = "2025-11-30T20:22:47.458Z" }, + { url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589, upload-time = "2025-11-30T20:22:48.872Z" }, + { url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025, upload-time = "2025-11-30T20:22:50.196Z" }, + { url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895, upload-time = "2025-11-30T20:22:51.87Z" }, + { url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799, upload-time = "2025-11-30T20:22:53.341Z" }, + { url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731, upload-time = "2025-11-30T20:22:54.778Z" }, + { url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027, upload-time = "2025-11-30T20:22:56.212Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020, upload-time = "2025-11-30T20:22:58.2Z" }, + { url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139, upload-time = "2025-11-30T20:23:00.209Z" }, + { url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224, upload-time = "2025-11-30T20:23:02.008Z" }, + { url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645, upload-time = "2025-11-30T20:23:03.43Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443, upload-time = "2025-11-30T20:23:04.878Z" }, + { url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375, upload-time = "2025-11-30T20:23:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850, upload-time = "2025-11-30T20:23:07.825Z" }, + { url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812, upload-time = "2025-11-30T20:23:09.228Z" }, + { url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841, upload-time = "2025-11-30T20:23:11.186Z" }, + { url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149, upload-time = "2025-11-30T20:23:12.864Z" }, + { url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843, upload-time = "2025-11-30T20:23:14.638Z" }, + { url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507, upload-time = "2025-11-30T20:23:16.105Z" }, + { url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949, upload-time = "2025-11-30T20:23:17.539Z" }, + { url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790, upload-time = "2025-11-30T20:23:19.029Z" }, + { url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217, upload-time = "2025-11-30T20:23:20.885Z" }, + { url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" }, + { url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341, upload-time = "2025-11-30T20:23:24.449Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768, upload-time = "2025-11-30T20:23:25.908Z" }, + { url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099, upload-time = "2025-11-30T20:23:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192, upload-time = "2025-11-30T20:23:29.151Z" }, + { url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080, upload-time = "2025-11-30T20:23:30.785Z" }, + { url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841, upload-time = "2025-11-30T20:23:32.209Z" }, + { url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670, upload-time = "2025-11-30T20:23:33.742Z" }, + { url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005, upload-time = "2025-11-30T20:23:35.253Z" }, + { url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112, upload-time = "2025-11-30T20:23:36.842Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049, upload-time = "2025-11-30T20:23:38.343Z" }, + { url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661, upload-time = "2025-11-30T20:23:40.263Z" }, + { url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606, upload-time = "2025-11-30T20:23:42.186Z" }, + { url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126, upload-time = "2025-11-30T20:23:44.086Z" }, + { url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371, upload-time = "2025-11-30T20:23:46.004Z" }, + { url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298, upload-time = "2025-11-30T20:23:47.696Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604, upload-time = "2025-11-30T20:23:49.501Z" }, + { url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391, upload-time = "2025-11-30T20:23:50.96Z" }, + { url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868, upload-time = "2025-11-30T20:23:52.494Z" }, + { url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747, upload-time = "2025-11-30T20:23:54.036Z" }, + { url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795, upload-time = "2025-11-30T20:23:55.556Z" }, + { url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330, upload-time = "2025-11-30T20:23:57.033Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194, upload-time = "2025-11-30T20:23:58.637Z" }, + { url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340, upload-time = "2025-11-30T20:24:00.2Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765, upload-time = "2025-11-30T20:24:01.759Z" }, + { url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834, upload-time = "2025-11-30T20:24:03.687Z" }, + { url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470, upload-time = "2025-11-30T20:24:05.232Z" }, + { url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630, upload-time = "2025-11-30T20:24:06.878Z" }, + { url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148, upload-time = "2025-11-30T20:24:08.445Z" }, + { url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030, upload-time = "2025-11-30T20:24:10.956Z" }, + { url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570, upload-time = "2025-11-30T20:24:12.735Z" }, + { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" }, + { url = "https://files.pythonhosted.org/packages/69/71/3f34339ee70521864411f8b6992e7ab13ac30d8e4e3309e07c7361767d91/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58", size = 372292, upload-time = "2025-11-30T20:24:16.537Z" }, + { url = "https://files.pythonhosted.org/packages/57/09/f183df9b8f2d66720d2ef71075c59f7e1b336bec7ee4c48f0a2b06857653/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a", size = 362128, upload-time = "2025-11-30T20:24:18.086Z" }, + { url = "https://files.pythonhosted.org/packages/7a/68/5c2594e937253457342e078f0cc1ded3dd7b2ad59afdbf2d354869110a02/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb", size = 391542, upload-time = "2025-11-30T20:24:20.092Z" }, + { url = "https://files.pythonhosted.org/packages/49/5c/31ef1afd70b4b4fbdb2800249f34c57c64beb687495b10aec0365f53dfc4/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c", size = 404004, upload-time = "2025-11-30T20:24:22.231Z" }, + { url = "https://files.pythonhosted.org/packages/e3/63/0cfbea38d05756f3440ce6534d51a491d26176ac045e2707adc99bb6e60a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3", size = 527063, upload-time = "2025-11-30T20:24:24.302Z" }, + { url = "https://files.pythonhosted.org/packages/42/e6/01e1f72a2456678b0f618fc9a1a13f882061690893c192fcad9f2926553a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5", size = 413099, upload-time = "2025-11-30T20:24:25.916Z" }, + { url = "https://files.pythonhosted.org/packages/b8/25/8df56677f209003dcbb180765520c544525e3ef21ea72279c98b9aa7c7fb/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738", size = 392177, upload-time = "2025-11-30T20:24:27.834Z" }, + { url = "https://files.pythonhosted.org/packages/4a/b4/0a771378c5f16f8115f796d1f437950158679bcd2a7c68cf251cfb00ed5b/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f", size = 406015, upload-time = "2025-11-30T20:24:29.457Z" }, + { url = "https://files.pythonhosted.org/packages/36/d8/456dbba0af75049dc6f63ff295a2f92766b9d521fa00de67a2bd6427d57a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877", size = 423736, upload-time = "2025-11-30T20:24:31.22Z" }, + { url = "https://files.pythonhosted.org/packages/13/64/b4d76f227d5c45a7e0b796c674fd81b0a6c4fbd48dc29271857d8219571c/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a", size = 573981, upload-time = "2025-11-30T20:24:32.934Z" }, + { url = "https://files.pythonhosted.org/packages/20/91/092bacadeda3edf92bf743cc96a7be133e13a39cdbfd7b5082e7ab638406/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4", size = 599782, upload-time = "2025-11-30T20:24:35.169Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b7/b95708304cd49b7b6f82fdd039f1748b66ec2b21d6a45180910802f1abf1/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e", size = 562191, upload-time = "2025-11-30T20:24:36.853Z" }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + +[[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, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "soupsieve" +version = "2.8.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/ae/2d9c981590ed9999a0d91755b47fc74f74de286b0f5cee14c9269041e6c4/soupsieve-2.8.3.tar.gz", hash = "sha256:3267f1eeea4251fb42728b6dfb746edc9acaffc4a45b27e19450b676586e8349", size = 118627, upload-time = "2026-01-20T04:27:02.457Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/2c/1462b1d0a634697ae9e55b3cecdcb64788e8b7d63f54d923fcd0bb140aed/soupsieve-2.8.3-py3-none-any.whl", hash = "sha256:ed64f2ba4eebeab06cc4962affce381647455978ffc1e36bb79a545b91f45a95", size = 37016, upload-time = "2026-01-20T04:27:01.012Z" }, +] + +[[package]] +name = "tinycss2" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "webencodings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7a/fd/7a5ee21fd08ff70d3d33a5781c255cbe779659bd03278feb98b19ee550f4/tinycss2-1.4.0.tar.gz", hash = "sha256:10c0972f6fc0fbee87c3edb76549357415e94548c1ae10ebccdea16fb404a9b7", size = 87085, upload-time = "2024-10-24T14:58:29.895Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/34/ebdc18bae6aa14fbee1a08b63c015c72b64868ff7dae68808ab500c492e2/tinycss2-1.4.0-py3-none-any.whl", hash = "sha256:3a49cf47b7675da0b15d0c6e1df8df4ebd96e9394bb905a5775adb0d884c5289", size = 26610, upload-time = "2024-10-24T14:58:28.029Z" }, +] + +[[package]] +name = "tornado" +version = "6.5.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/f1/3173dfa4a18db4a9b03e5d55325559dab51ee653763bb8745a75af491286/tornado-6.5.5.tar.gz", hash = "sha256:192b8f3ea91bd7f1f50c06955416ed76c6b72f96779b962f07f911b91e8d30e9", size = 516006, upload-time = "2026-03-10T21:31:02.067Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/8c/77f5097695f4dd8255ecbd08b2a1ed8ba8b953d337804dd7080f199e12bf/tornado-6.5.5-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:487dc9cc380e29f58c7ab88f9e27cdeef04b2140862e5076a66fb6bb68bb1bfa", size = 445983, upload-time = "2026-03-10T21:30:44.28Z" }, + { url = "https://files.pythonhosted.org/packages/ab/5e/7625b76cd10f98f1516c36ce0346de62061156352353ef2da44e5c21523c/tornado-6.5.5-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:65a7f1d46d4bb41df1ac99f5fcb685fb25c7e61613742d5108b010975a9a6521", size = 444246, upload-time = "2026-03-10T21:30:46.571Z" }, + { url = "https://files.pythonhosted.org/packages/b2/04/7b5705d5b3c0fab088f434f9c83edac1573830ca49ccf29fb83bf7178eec/tornado-6.5.5-cp39-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e74c92e8e65086b338fd56333fb9a68b9f6f2fe7ad532645a290a464bcf46be5", size = 447229, upload-time = "2026-03-10T21:30:48.273Z" }, + { url = "https://files.pythonhosted.org/packages/34/01/74e034a30ef59afb4097ef8659515e96a39d910b712a89af76f5e4e1f93c/tornado-6.5.5-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:435319e9e340276428bbdb4e7fa732c2d399386d1de5686cb331ec8eee754f07", size = 448192, upload-time = "2026-03-10T21:30:51.22Z" }, + { url = "https://files.pythonhosted.org/packages/be/00/fe9e02c5a96429fce1a1d15a517f5d8444f9c412e0bb9eadfbe3b0fc55bf/tornado-6.5.5-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:3f54aa540bdbfee7b9eb268ead60e7d199de5021facd276819c193c0fb28ea4e", size = 448039, upload-time = "2026-03-10T21:30:53.52Z" }, + { url = "https://files.pythonhosted.org/packages/82/9e/656ee4cec0398b1d18d0f1eb6372c41c6b889722641d84948351ae19556d/tornado-6.5.5-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:36abed1754faeb80fbd6e64db2758091e1320f6bba74a4cf8c09cd18ccce8aca", size = 447445, upload-time = "2026-03-10T21:30:55.541Z" }, + { url = "https://files.pythonhosted.org/packages/5a/76/4921c00511f88af86a33de770d64141170f1cfd9c00311aea689949e274e/tornado-6.5.5-cp39-abi3-win32.whl", hash = "sha256:dd3eafaaeec1c7f2f8fdcd5f964e8907ad788fe8a5a32c4426fbbdda621223b7", size = 448582, upload-time = "2026-03-10T21:30:57.142Z" }, + { url = "https://files.pythonhosted.org/packages/2c/23/f6c6112a04d28eed765e374435fb1a9198f73e1ec4b4024184f21faeb1ad/tornado-6.5.5-cp39-abi3-win_amd64.whl", hash = "sha256:6443a794ba961a9f619b1ae926a2e900ac20c34483eea67be4ed8f1e58d3ef7b", size = 448990, upload-time = "2026-03-10T21:30:58.857Z" }, + { url = "https://files.pythonhosted.org/packages/b7/c8/876602cbc96469911f0939f703453c1157b0c826ecb05bdd32e023397d4e/tornado-6.5.5-cp39-abi3-win_arm64.whl", hash = "sha256:2c9a876e094109333f888539ddb2de4361743e5d21eece20688e3e351e4990a6", size = 448016, upload-time = "2026-03-10T21:31:00.43Z" }, +] + +[[package]] +name = "traitlets" +version = "5.14.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/eb/79/72064e6a701c2183016abbbfedaba506d81e30e232a68c9f0d6f6fcd1574/traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7", size = 161621, upload-time = "2024-04-19T11:11:49.746Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359, upload-time = "2024-04-19T11:11:46.763Z" }, +] + +[[package]] +name = "typer" +version = "0.24.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/b8/9ebb531b6c2d377af08ac6746a5df3425b21853a5d2260876919b58a2a4a/typer-0.24.2.tar.gz", hash = "sha256:ec070dcfca1408e85ee203c6365001e818c3b7fffe686fd07ff2d68095ca0480", size = 119849, upload-time = "2026-04-22T17:45:34.413Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/d1/9484b497e0a0410b901c12b8251c3e746e1e863f7d28419ffe06f7892fda/typer-0.24.2-py3-none-any.whl", hash = "sha256:b618bc3d721f9a8d30f3e05565be26416d06e9bcc29d49bc491dc26aba674fa8", size = 55977, upload-time = "2026-04-22T17:45:33.055Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "webencodings" +version = "0.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/02/ae6ceac1baeda530866a85075641cec12989bd8d31af6d5ab4a3e8c92f47/webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923", size = 9721, upload-time = "2017-04-05T20:21:34.189Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", size = 11774, upload-time = "2017-04-05T20:21:32.581Z" }, +] + +[[package]] +name = "websockets" +version = "16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", size = 179346, upload-time = "2026-01-10T09:23:47.181Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/db/de907251b4ff46ae804ad0409809504153b3f30984daf82a1d84a9875830/websockets-16.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:31a52addea25187bde0797a97d6fc3d2f92b6f72a9370792d65a6e84615ac8a8", size = 177340, upload-time = "2026-01-10T09:22:34.539Z" }, + { url = "https://files.pythonhosted.org/packages/f3/fa/abe89019d8d8815c8781e90d697dec52523fb8ebe308bf11664e8de1877e/websockets-16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:417b28978cdccab24f46400586d128366313e8a96312e4b9362a4af504f3bbad", size = 175022, upload-time = "2026-01-10T09:22:36.332Z" }, + { url = "https://files.pythonhosted.org/packages/58/5d/88ea17ed1ded2079358b40d31d48abe90a73c9e5819dbcde1606e991e2ad/websockets-16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:af80d74d4edfa3cb9ed973a0a5ba2b2a549371f8a741e0800cb07becdd20f23d", size = 175319, upload-time = "2026-01-10T09:22:37.602Z" }, + { url = "https://files.pythonhosted.org/packages/d2/ae/0ee92b33087a33632f37a635e11e1d99d429d3d323329675a6022312aac2/websockets-16.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:08d7af67b64d29823fed316505a89b86705f2b7981c07848fb5e3ea3020c1abe", size = 184631, upload-time = "2026-01-10T09:22:38.789Z" }, + { url = "https://files.pythonhosted.org/packages/c8/c5/27178df583b6c5b31b29f526ba2da5e2f864ecc79c99dae630a85d68c304/websockets-16.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7be95cfb0a4dae143eaed2bcba8ac23f4892d8971311f1b06f3c6b78952ee70b", size = 185870, upload-time = "2026-01-10T09:22:39.893Z" }, + { url = "https://files.pythonhosted.org/packages/87/05/536652aa84ddc1c018dbb7e2c4cbcd0db884580bf8e95aece7593fde526f/websockets-16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d6297ce39ce5c2e6feb13c1a996a2ded3b6832155fcfc920265c76f24c7cceb5", size = 185361, upload-time = "2026-01-10T09:22:41.016Z" }, + { url = "https://files.pythonhosted.org/packages/6d/e2/d5332c90da12b1e01f06fb1b85c50cfc489783076547415bf9f0a659ec19/websockets-16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1c1b30e4f497b0b354057f3467f56244c603a79c0d1dafce1d16c283c25f6e64", size = 184615, upload-time = "2026-01-10T09:22:42.442Z" }, + { url = "https://files.pythonhosted.org/packages/77/fb/d3f9576691cae9253b51555f841bc6600bf0a983a461c79500ace5a5b364/websockets-16.0-cp311-cp311-win32.whl", hash = "sha256:5f451484aeb5cafee1ccf789b1b66f535409d038c56966d6101740c1614b86c6", size = 178246, upload-time = "2026-01-10T09:22:43.654Z" }, + { url = "https://files.pythonhosted.org/packages/54/67/eaff76b3dbaf18dcddabc3b8c1dba50b483761cccff67793897945b37408/websockets-16.0-cp311-cp311-win_amd64.whl", hash = "sha256:8d7f0659570eefb578dacde98e24fb60af35350193e4f56e11190787bee77dac", size = 178684, upload-time = "2026-01-10T09:22:44.941Z" }, + { url = "https://files.pythonhosted.org/packages/84/7b/bac442e6b96c9d25092695578dda82403c77936104b5682307bd4deb1ad4/websockets-16.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:71c989cbf3254fbd5e84d3bff31e4da39c43f884e64f2551d14bb3c186230f00", size = 177365, upload-time = "2026-01-10T09:22:46.787Z" }, + { url = "https://files.pythonhosted.org/packages/b0/fe/136ccece61bd690d9c1f715baaeefd953bb2360134de73519d5df19d29ca/websockets-16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8b6e209ffee39ff1b6d0fa7bfef6de950c60dfb91b8fcead17da4ee539121a79", size = 175038, upload-time = "2026-01-10T09:22:47.999Z" }, + { url = "https://files.pythonhosted.org/packages/40/1e/9771421ac2286eaab95b8575b0cb701ae3663abf8b5e1f64f1fd90d0a673/websockets-16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86890e837d61574c92a97496d590968b23c2ef0aeb8a9bc9421d174cd378ae39", size = 175328, upload-time = "2026-01-10T09:22:49.809Z" }, + { url = "https://files.pythonhosted.org/packages/18/29/71729b4671f21e1eaa5d6573031ab810ad2936c8175f03f97f3ff164c802/websockets-16.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9b5aca38b67492ef518a8ab76851862488a478602229112c4b0d58d63a7a4d5c", size = 184915, upload-time = "2026-01-10T09:22:51.071Z" }, + { url = "https://files.pythonhosted.org/packages/97/bb/21c36b7dbbafc85d2d480cd65df02a1dc93bf76d97147605a8e27ff9409d/websockets-16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e0334872c0a37b606418ac52f6ab9cfd17317ac26365f7f65e203e2d0d0d359f", size = 186152, upload-time = "2026-01-10T09:22:52.224Z" }, + { url = "https://files.pythonhosted.org/packages/4a/34/9bf8df0c0cf88fa7bfe36678dc7b02970c9a7d5e065a3099292db87b1be2/websockets-16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a0b31e0b424cc6b5a04b8838bbaec1688834b2383256688cf47eb97412531da1", size = 185583, upload-time = "2026-01-10T09:22:53.443Z" }, + { url = "https://files.pythonhosted.org/packages/47/88/4dd516068e1a3d6ab3c7c183288404cd424a9a02d585efbac226cb61ff2d/websockets-16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:485c49116d0af10ac698623c513c1cc01c9446c058a4e61e3bf6c19dff7335a2", size = 184880, upload-time = "2026-01-10T09:22:55.033Z" }, + { url = "https://files.pythonhosted.org/packages/91/d6/7d4553ad4bf1c0421e1ebd4b18de5d9098383b5caa1d937b63df8d04b565/websockets-16.0-cp312-cp312-win32.whl", hash = "sha256:eaded469f5e5b7294e2bdca0ab06becb6756ea86894a47806456089298813c89", size = 178261, upload-time = "2026-01-10T09:22:56.251Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f0/f3a17365441ed1c27f850a80b2bc680a0fa9505d733fe152fdf5e98c1c0b/websockets-16.0-cp312-cp312-win_amd64.whl", hash = "sha256:5569417dc80977fc8c2d43a86f78e0a5a22fee17565d78621b6bb264a115d4ea", size = 178693, upload-time = "2026-01-10T09:22:57.478Z" }, + { url = "https://files.pythonhosted.org/packages/cc/9c/baa8456050d1c1b08dd0ec7346026668cbc6f145ab4e314d707bb845bf0d/websockets-16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:878b336ac47938b474c8f982ac2f7266a540adc3fa4ad74ae96fea9823a02cc9", size = 177364, upload-time = "2026-01-10T09:22:59.333Z" }, + { url = "https://files.pythonhosted.org/packages/7e/0c/8811fc53e9bcff68fe7de2bcbe75116a8d959ac699a3200f4847a8925210/websockets-16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:52a0fec0e6c8d9a784c2c78276a48a2bdf099e4ccc2a4cad53b27718dbfd0230", size = 175039, upload-time = "2026-01-10T09:23:01.171Z" }, + { url = "https://files.pythonhosted.org/packages/aa/82/39a5f910cb99ec0b59e482971238c845af9220d3ab9fa76dd9162cda9d62/websockets-16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6578ed5b6981005df1860a56e3617f14a6c307e6a71b4fff8c48fdc50f3ed2c", size = 175323, upload-time = "2026-01-10T09:23:02.341Z" }, + { url = "https://files.pythonhosted.org/packages/bd/28/0a25ee5342eb5d5f297d992a77e56892ecb65e7854c7898fb7d35e9b33bd/websockets-16.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95724e638f0f9c350bb1c2b0a7ad0e83d9cc0c9259f3ea94e40d7b02a2179ae5", size = 184975, upload-time = "2026-01-10T09:23:03.756Z" }, + { url = "https://files.pythonhosted.org/packages/f9/66/27ea52741752f5107c2e41fda05e8395a682a1e11c4e592a809a90c6a506/websockets-16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0204dc62a89dc9d50d682412c10b3542d748260d743500a85c13cd1ee4bde82", size = 186203, upload-time = "2026-01-10T09:23:05.01Z" }, + { url = "https://files.pythonhosted.org/packages/37/e5/8e32857371406a757816a2b471939d51c463509be73fa538216ea52b792a/websockets-16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52ac480f44d32970d66763115edea932f1c5b1312de36df06d6b219f6741eed8", size = 185653, upload-time = "2026-01-10T09:23:06.301Z" }, + { url = "https://files.pythonhosted.org/packages/9b/67/f926bac29882894669368dc73f4da900fcdf47955d0a0185d60103df5737/websockets-16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6e5a82b677f8f6f59e8dfc34ec06ca6b5b48bc4fcda346acd093694cc2c24d8f", size = 184920, upload-time = "2026-01-10T09:23:07.492Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a1/3d6ccdcd125b0a42a311bcd15a7f705d688f73b2a22d8cf1c0875d35d34a/websockets-16.0-cp313-cp313-win32.whl", hash = "sha256:abf050a199613f64c886ea10f38b47770a65154dc37181bfaff70c160f45315a", size = 178255, upload-time = "2026-01-10T09:23:09.245Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ae/90366304d7c2ce80f9b826096a9e9048b4bb760e44d3b873bb272cba696b/websockets-16.0-cp313-cp313-win_amd64.whl", hash = "sha256:3425ac5cf448801335d6fdc7ae1eb22072055417a96cc6b31b3861f455fbc156", size = 178689, upload-time = "2026-01-10T09:23:10.483Z" }, + { url = "https://files.pythonhosted.org/packages/f3/1d/e88022630271f5bd349ed82417136281931e558d628dd52c4d8621b4a0b2/websockets-16.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0", size = 177406, upload-time = "2026-01-10T09:23:12.178Z" }, + { url = "https://files.pythonhosted.org/packages/f2/78/e63be1bf0724eeb4616efb1ae1c9044f7c3953b7957799abb5915bffd38e/websockets-16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:daa3b6ff70a9241cf6c7fc9e949d41232d9d7d26fd3522b1ad2b4d62487e9904", size = 175085, upload-time = "2026-01-10T09:23:13.511Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f4/d3c9220d818ee955ae390cf319a7c7a467beceb24f05ee7aaaa2414345ba/websockets-16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4", size = 175328, upload-time = "2026-01-10T09:23:14.727Z" }, + { url = "https://files.pythonhosted.org/packages/63/bc/d3e208028de777087e6fb2b122051a6ff7bbcca0d6df9d9c2bf1dd869ae9/websockets-16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:781caf5e8eee67f663126490c2f96f40906594cb86b408a703630f95550a8c3e", size = 185044, upload-time = "2026-01-10T09:23:15.939Z" }, + { url = "https://files.pythonhosted.org/packages/ad/6e/9a0927ac24bd33a0a9af834d89e0abc7cfd8e13bed17a86407a66773cc0e/websockets-16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caab51a72c51973ca21fa8a18bd8165e1a0183f1ac7066a182ff27107b71e1a4", size = 186279, upload-time = "2026-01-10T09:23:17.148Z" }, + { url = "https://files.pythonhosted.org/packages/b9/ca/bf1c68440d7a868180e11be653c85959502efd3a709323230314fda6e0b3/websockets-16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1", size = 185711, upload-time = "2026-01-10T09:23:18.372Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f8/fdc34643a989561f217bb477cbc47a3a07212cbda91c0e4389c43c296ebf/websockets-16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5e18a238a2b2249c9a9235466b90e96ae4795672598a58772dd806edc7ac6d3", size = 184982, upload-time = "2026-01-10T09:23:19.652Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d1/574fa27e233764dbac9c52730d63fcf2823b16f0856b3329fc6268d6ae4f/websockets-16.0-cp314-cp314-win32.whl", hash = "sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8", size = 177915, upload-time = "2026-01-10T09:23:21.458Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f1/ae6b937bf3126b5134ce1f482365fde31a357c784ac51852978768b5eff4/websockets-16.0-cp314-cp314-win_amd64.whl", hash = "sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d", size = 178381, upload-time = "2026-01-10T09:23:22.715Z" }, + { url = "https://files.pythonhosted.org/packages/06/9b/f791d1db48403e1f0a27577a6beb37afae94254a8c6f08be4a23e4930bc0/websockets-16.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a35539cacc3febb22b8f4d4a99cc79b104226a756aa7400adc722e83b0d03244", size = 177737, upload-time = "2026-01-10T09:23:24.523Z" }, + { url = "https://files.pythonhosted.org/packages/bd/40/53ad02341fa33b3ce489023f635367a4ac98b73570102ad2cdd770dacc9a/websockets-16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b784ca5de850f4ce93ec85d3269d24d4c82f22b7212023c974c401d4980ebc5e", size = 175268, upload-time = "2026-01-10T09:23:25.781Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/6158d4e459b984f949dcbbb0c5d270154c7618e11c01029b9bbd1bb4c4f9/websockets-16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:569d01a4e7fba956c5ae4fc988f0d4e187900f5497ce46339c996dbf24f17641", size = 175486, upload-time = "2026-01-10T09:23:27.033Z" }, + { url = "https://files.pythonhosted.org/packages/e5/2d/7583b30208b639c8090206f95073646c2c9ffd66f44df967981a64f849ad/websockets-16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50f23cdd8343b984957e4077839841146f67a3d31ab0d00e6b824e74c5b2f6e8", size = 185331, upload-time = "2026-01-10T09:23:28.259Z" }, + { url = "https://files.pythonhosted.org/packages/45/b0/cce3784eb519b7b5ad680d14b9673a31ab8dcb7aad8b64d81709d2430aa8/websockets-16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:152284a83a00c59b759697b7f9e9cddf4e3c7861dd0d964b472b70f78f89e80e", size = 186501, upload-time = "2026-01-10T09:23:29.449Z" }, + { url = "https://files.pythonhosted.org/packages/19/60/b8ebe4c7e89fb5f6cdf080623c9d92789a53636950f7abacfc33fe2b3135/websockets-16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944", size = 186062, upload-time = "2026-01-10T09:23:31.368Z" }, + { url = "https://files.pythonhosted.org/packages/88/a8/a080593f89b0138b6cba1b28f8df5673b5506f72879322288b031337c0b8/websockets-16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206", size = 185356, upload-time = "2026-01-10T09:23:32.627Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b6/b9afed2afadddaf5ebb2afa801abf4b0868f42f8539bfe4b071b5266c9fe/websockets-16.0-cp314-cp314t-win32.whl", hash = "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6", size = 178085, upload-time = "2026-01-10T09:23:33.816Z" }, + { url = "https://files.pythonhosted.org/packages/9f/3e/28135a24e384493fa804216b79a6a6759a38cc4ff59118787b9fb693df93/websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd", size = 178531, upload-time = "2026-01-10T09:23:35.016Z" }, + { url = "https://files.pythonhosted.org/packages/72/07/c98a68571dcf256e74f1f816b8cc5eae6eb2d3d5cfa44d37f801619d9166/websockets-16.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:349f83cd6c9a415428ee1005cadb5c2c56f4389bc06a9af16103c3bc3dcc8b7d", size = 174947, upload-time = "2026-01-10T09:23:36.166Z" }, + { url = "https://files.pythonhosted.org/packages/7e/52/93e166a81e0305b33fe416338be92ae863563fe7bce446b0f687b9df5aea/websockets-16.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:4a1aba3340a8dca8db6eb5a7986157f52eb9e436b74813764241981ca4888f03", size = 175260, upload-time = "2026-01-10T09:23:37.409Z" }, + { url = "https://files.pythonhosted.org/packages/56/0c/2dbf513bafd24889d33de2ff0368190a0e69f37bcfa19009ef819fe4d507/websockets-16.0-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f4a32d1bd841d4bcbffdcb3d2ce50c09c3909fbead375ab28d0181af89fd04da", size = 176071, upload-time = "2026-01-10T09:23:39.158Z" }, + { url = "https://files.pythonhosted.org/packages/a5/8f/aea9c71cc92bf9b6cc0f7f70df8f0b420636b6c96ef4feee1e16f80f75dd/websockets-16.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0298d07ee155e2e9fda5be8a9042200dd2e3bb0b8a38482156576f863a9d457c", size = 176968, upload-time = "2026-01-10T09:23:41.031Z" }, + { url = "https://files.pythonhosted.org/packages/9a/3f/f70e03f40ffc9a30d817eef7da1be72ee4956ba8d7255c399a01b135902a/websockets-16.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:a653aea902e0324b52f1613332ddf50b00c06fdaf7e92624fbf8c77c78fa5767", size = 178735, upload-time = "2026-01-10T09:23:42.259Z" }, + { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" }, +] + +[[package]] +name = "xxhash" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/02/84/30869e01909fb37a6cc7e18688ee8bf1e42d57e7e0777636bd47524c43c7/xxhash-3.6.0.tar.gz", hash = "sha256:f0162a78b13a0d7617b2845b90c763339d1f1d82bb04a4b07f4ab535cc5e05d6", size = 85160, upload-time = "2025-10-02T14:37:08.097Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/d4/cc2f0400e9154df4b9964249da78ebd72f318e35ccc425e9f403c392f22a/xxhash-3.6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b47bbd8cf2d72797f3c2772eaaac0ded3d3af26481a26d7d7d41dc2d3c46b04a", size = 32844, upload-time = "2025-10-02T14:34:14.037Z" }, + { url = "https://files.pythonhosted.org/packages/5e/ec/1cc11cd13e26ea8bc3cb4af4eaadd8d46d5014aebb67be3f71fb0b68802a/xxhash-3.6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2b6821e94346f96db75abaa6e255706fb06ebd530899ed76d32cd99f20dc52fa", size = 30809, upload-time = "2025-10-02T14:34:15.484Z" }, + { url = "https://files.pythonhosted.org/packages/04/5f/19fe357ea348d98ca22f456f75a30ac0916b51c753e1f8b2e0e6fb884cce/xxhash-3.6.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d0a9751f71a1a65ce3584e9cae4467651c7e70c9d31017fa57574583a4540248", size = 194665, upload-time = "2025-10-02T14:34:16.541Z" }, + { url = "https://files.pythonhosted.org/packages/90/3b/d1f1a8f5442a5fd8beedae110c5af7604dc37349a8e16519c13c19a9a2de/xxhash-3.6.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b29ee68625ab37b04c0b40c3fafdf24d2f75ccd778333cfb698f65f6c463f62", size = 213550, upload-time = "2025-10-02T14:34:17.878Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ef/3a9b05eb527457d5db13a135a2ae1a26c80fecd624d20f3e8dcc4cb170f3/xxhash-3.6.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6812c25fe0d6c36a46ccb002f40f27ac903bf18af9f6dd8f9669cb4d176ab18f", size = 212384, upload-time = "2025-10-02T14:34:19.182Z" }, + { url = "https://files.pythonhosted.org/packages/0f/18/ccc194ee698c6c623acbf0f8c2969811a8a4b6185af5e824cd27b9e4fd3e/xxhash-3.6.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4ccbff013972390b51a18ef1255ef5ac125c92dc9143b2d1909f59abc765540e", size = 445749, upload-time = "2025-10-02T14:34:20.659Z" }, + { url = "https://files.pythonhosted.org/packages/a5/86/cf2c0321dc3940a7aa73076f4fd677a0fb3e405cb297ead7d864fd90847e/xxhash-3.6.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:297b7fbf86c82c550e12e8fb71968b3f033d27b874276ba3624ea868c11165a8", size = 193880, upload-time = "2025-10-02T14:34:22.431Z" }, + { url = "https://files.pythonhosted.org/packages/82/fb/96213c8560e6f948a1ecc9a7613f8032b19ee45f747f4fca4eb31bb6d6ed/xxhash-3.6.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dea26ae1eb293db089798d3973a5fc928a18fdd97cc8801226fae705b02b14b0", size = 210912, upload-time = "2025-10-02T14:34:23.937Z" }, + { url = "https://files.pythonhosted.org/packages/40/aa/4395e669b0606a096d6788f40dbdf2b819d6773aa290c19e6e83cbfc312f/xxhash-3.6.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7a0b169aafb98f4284f73635a8e93f0735f9cbde17bd5ec332480484241aaa77", size = 198654, upload-time = "2025-10-02T14:34:25.644Z" }, + { url = "https://files.pythonhosted.org/packages/67/74/b044fcd6b3d89e9b1b665924d85d3f400636c23590226feb1eb09e1176ce/xxhash-3.6.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:08d45aef063a4531b785cd72de4887766d01dc8f362a515693df349fdb825e0c", size = 210867, upload-time = "2025-10-02T14:34:27.203Z" }, + { url = "https://files.pythonhosted.org/packages/bc/fd/3ce73bf753b08cb19daee1eb14aa0d7fe331f8da9c02dd95316ddfe5275e/xxhash-3.6.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:929142361a48ee07f09121fe9e96a84950e8d4df3bb298ca5d88061969f34d7b", size = 414012, upload-time = "2025-10-02T14:34:28.409Z" }, + { url = "https://files.pythonhosted.org/packages/ba/b3/5a4241309217c5c876f156b10778f3ab3af7ba7e3259e6d5f5c7d0129eb2/xxhash-3.6.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:51312c768403d8540487dbbfb557454cfc55589bbde6424456951f7fcd4facb3", size = 191409, upload-time = "2025-10-02T14:34:29.696Z" }, + { url = "https://files.pythonhosted.org/packages/c0/01/99bfbc15fb9abb9a72b088c1d95219fc4782b7d01fc835bd5744d66dd0b8/xxhash-3.6.0-cp311-cp311-win32.whl", hash = "sha256:d1927a69feddc24c987b337ce81ac15c4720955b667fe9b588e02254b80446fd", size = 30574, upload-time = "2025-10-02T14:34:31.028Z" }, + { url = "https://files.pythonhosted.org/packages/65/79/9d24d7f53819fe301b231044ea362ce64e86c74f6e8c8e51320de248b3e5/xxhash-3.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:26734cdc2d4ffe449b41d186bbeac416f704a482ed835d375a5c0cb02bc63fef", size = 31481, upload-time = "2025-10-02T14:34:32.062Z" }, + { url = "https://files.pythonhosted.org/packages/30/4e/15cd0e3e8772071344eab2961ce83f6e485111fed8beb491a3f1ce100270/xxhash-3.6.0-cp311-cp311-win_arm64.whl", hash = "sha256:d72f67ef8bf36e05f5b6c65e8524f265bd61071471cd4cf1d36743ebeeeb06b7", size = 27861, upload-time = "2025-10-02T14:34:33.555Z" }, + { url = "https://files.pythonhosted.org/packages/9a/07/d9412f3d7d462347e4511181dea65e47e0d0e16e26fbee2ea86a2aefb657/xxhash-3.6.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:01362c4331775398e7bb34e3ab403bc9ee9f7c497bc7dee6272114055277dd3c", size = 32744, upload-time = "2025-10-02T14:34:34.622Z" }, + { url = "https://files.pythonhosted.org/packages/79/35/0429ee11d035fc33abe32dca1b2b69e8c18d236547b9a9b72c1929189b9a/xxhash-3.6.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b7b2df81a23f8cb99656378e72501b2cb41b1827c0f5a86f87d6b06b69f9f204", size = 30816, upload-time = "2025-10-02T14:34:36.043Z" }, + { url = "https://files.pythonhosted.org/packages/b7/f2/57eb99aa0f7d98624c0932c5b9a170e1806406cdbcdb510546634a1359e0/xxhash-3.6.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:dc94790144e66b14f67b10ac8ed75b39ca47536bf8800eb7c24b50271ea0c490", size = 194035, upload-time = "2025-10-02T14:34:37.354Z" }, + { url = "https://files.pythonhosted.org/packages/4c/ed/6224ba353690d73af7a3f1c7cdb1fc1b002e38f783cb991ae338e1eb3d79/xxhash-3.6.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:93f107c673bccf0d592cdba077dedaf52fe7f42dcd7676eba1f6d6f0c3efffd2", size = 212914, upload-time = "2025-10-02T14:34:38.6Z" }, + { url = "https://files.pythonhosted.org/packages/38/86/fb6b6130d8dd6b8942cc17ab4d90e223653a89aa32ad2776f8af7064ed13/xxhash-3.6.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2aa5ee3444c25b69813663c9f8067dcfaa2e126dc55e8dddf40f4d1c25d7effa", size = 212163, upload-time = "2025-10-02T14:34:39.872Z" }, + { url = "https://files.pythonhosted.org/packages/ee/dc/e84875682b0593e884ad73b2d40767b5790d417bde603cceb6878901d647/xxhash-3.6.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7f99123f0e1194fa59cc69ad46dbae2e07becec5df50a0509a808f90a0f03f0", size = 445411, upload-time = "2025-10-02T14:34:41.569Z" }, + { url = "https://files.pythonhosted.org/packages/11/4f/426f91b96701ec2f37bb2b8cec664eff4f658a11f3fa9d94f0a887ea6d2b/xxhash-3.6.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:49e03e6fe2cac4a1bc64952dd250cf0dbc5ef4ebb7b8d96bce82e2de163c82a2", size = 193883, upload-time = "2025-10-02T14:34:43.249Z" }, + { url = "https://files.pythonhosted.org/packages/53/5a/ddbb83eee8e28b778eacfc5a85c969673e4023cdeedcfcef61f36731610b/xxhash-3.6.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bd17fede52a17a4f9a7bc4472a5867cb0b160deeb431795c0e4abe158bc784e9", size = 210392, upload-time = "2025-10-02T14:34:45.042Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c2/ff69efd07c8c074ccdf0a4f36fcdd3d27363665bcdf4ba399abebe643465/xxhash-3.6.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:6fb5f5476bef678f69db04f2bd1efbed3030d2aba305b0fc1773645f187d6a4e", size = 197898, upload-time = "2025-10-02T14:34:46.302Z" }, + { url = "https://files.pythonhosted.org/packages/58/ca/faa05ac19b3b622c7c9317ac3e23954187516298a091eb02c976d0d3dd45/xxhash-3.6.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:843b52f6d88071f87eba1631b684fcb4b2068cd2180a0224122fe4ef011a9374", size = 210655, upload-time = "2025-10-02T14:34:47.571Z" }, + { url = "https://files.pythonhosted.org/packages/d4/7a/06aa7482345480cc0cb597f5c875b11a82c3953f534394f620b0be2f700c/xxhash-3.6.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7d14a6cfaf03b1b6f5f9790f76880601ccc7896aff7ab9cd8978a939c1eb7e0d", size = 414001, upload-time = "2025-10-02T14:34:49.273Z" }, + { url = "https://files.pythonhosted.org/packages/23/07/63ffb386cd47029aa2916b3d2f454e6cc5b9f5c5ada3790377d5430084e7/xxhash-3.6.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:418daf3db71e1413cfe211c2f9a528456936645c17f46b5204705581a45390ae", size = 191431, upload-time = "2025-10-02T14:34:50.798Z" }, + { url = "https://files.pythonhosted.org/packages/0f/93/14fde614cadb4ddf5e7cebf8918b7e8fac5ae7861c1875964f17e678205c/xxhash-3.6.0-cp312-cp312-win32.whl", hash = "sha256:50fc255f39428a27299c20e280d6193d8b63b8ef8028995323bf834a026b4fbb", size = 30617, upload-time = "2025-10-02T14:34:51.954Z" }, + { url = "https://files.pythonhosted.org/packages/13/5d/0d125536cbe7565a83d06e43783389ecae0c0f2ed037b48ede185de477c0/xxhash-3.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:c0f2ab8c715630565ab8991b536ecded9416d615538be8ecddce43ccf26cbc7c", size = 31534, upload-time = "2025-10-02T14:34:53.276Z" }, + { url = "https://files.pythonhosted.org/packages/54/85/6ec269b0952ec7e36ba019125982cf11d91256a778c7c3f98a4c5043d283/xxhash-3.6.0-cp312-cp312-win_arm64.whl", hash = "sha256:eae5c13f3bc455a3bbb68bdc513912dc7356de7e2280363ea235f71f54064829", size = 27876, upload-time = "2025-10-02T14:34:54.371Z" }, + { url = "https://files.pythonhosted.org/packages/33/76/35d05267ac82f53ae9b0e554da7c5e281ee61f3cad44c743f0fcd354f211/xxhash-3.6.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:599e64ba7f67472481ceb6ee80fa3bd828fd61ba59fb11475572cc5ee52b89ec", size = 32738, upload-time = "2025-10-02T14:34:55.839Z" }, + { url = "https://files.pythonhosted.org/packages/31/a8/3fbce1cd96534a95e35d5120637bf29b0d7f5d8fa2f6374e31b4156dd419/xxhash-3.6.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7d8b8aaa30fca4f16f0c84a5c8d7ddee0e25250ec2796c973775373257dde8f1", size = 30821, upload-time = "2025-10-02T14:34:57.219Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ea/d387530ca7ecfa183cb358027f1833297c6ac6098223fd14f9782cd0015c/xxhash-3.6.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d597acf8506d6e7101a4a44a5e428977a51c0fadbbfd3c39650cca9253f6e5a6", size = 194127, upload-time = "2025-10-02T14:34:59.21Z" }, + { url = "https://files.pythonhosted.org/packages/ba/0c/71435dcb99874b09a43b8d7c54071e600a7481e42b3e3ce1eb5226a5711a/xxhash-3.6.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:858dc935963a33bc33490128edc1c12b0c14d9c7ebaa4e387a7869ecc4f3e263", size = 212975, upload-time = "2025-10-02T14:35:00.816Z" }, + { url = "https://files.pythonhosted.org/packages/84/7a/c2b3d071e4bb4a90b7057228a99b10d51744878f4a8a6dd643c8bd897620/xxhash-3.6.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba284920194615cb8edf73bf52236ce2e1664ccd4a38fdb543506413529cc546", size = 212241, upload-time = "2025-10-02T14:35:02.207Z" }, + { url = "https://files.pythonhosted.org/packages/81/5f/640b6eac0128e215f177df99eadcd0f1b7c42c274ab6a394a05059694c5a/xxhash-3.6.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4b54219177f6c6674d5378bd862c6aedf64725f70dd29c472eaae154df1a2e89", size = 445471, upload-time = "2025-10-02T14:35:03.61Z" }, + { url = "https://files.pythonhosted.org/packages/5e/1e/3c3d3ef071b051cc3abbe3721ffb8365033a172613c04af2da89d5548a87/xxhash-3.6.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:42c36dd7dbad2f5238950c377fcbf6811b1cdb1c444fab447960030cea60504d", size = 193936, upload-time = "2025-10-02T14:35:05.013Z" }, + { url = "https://files.pythonhosted.org/packages/2c/bd/4a5f68381939219abfe1c22a9e3a5854a4f6f6f3c4983a87d255f21f2e5d/xxhash-3.6.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f22927652cba98c44639ffdc7aaf35828dccf679b10b31c4ad72a5b530a18eb7", size = 210440, upload-time = "2025-10-02T14:35:06.239Z" }, + { url = "https://files.pythonhosted.org/packages/eb/37/b80fe3d5cfb9faff01a02121a0f4d565eb7237e9e5fc66e73017e74dcd36/xxhash-3.6.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b45fad44d9c5c119e9c6fbf2e1c656a46dc68e280275007bbfd3d572b21426db", size = 197990, upload-time = "2025-10-02T14:35:07.735Z" }, + { url = "https://files.pythonhosted.org/packages/d7/fd/2c0a00c97b9e18f72e1f240ad4e8f8a90fd9d408289ba9c7c495ed7dc05c/xxhash-3.6.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:6f2580ffab1a8b68ef2b901cde7e55fa8da5e4be0977c68f78fc80f3c143de42", size = 210689, upload-time = "2025-10-02T14:35:09.438Z" }, + { url = "https://files.pythonhosted.org/packages/93/86/5dd8076a926b9a95db3206aba20d89a7fc14dd5aac16e5c4de4b56033140/xxhash-3.6.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:40c391dd3cd041ebc3ffe6f2c862f402e306eb571422e0aa918d8070ba31da11", size = 414068, upload-time = "2025-10-02T14:35:11.162Z" }, + { url = "https://files.pythonhosted.org/packages/af/3c/0bb129170ee8f3650f08e993baee550a09593462a5cddd8e44d0011102b1/xxhash-3.6.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f205badabde7aafd1a31e8ca2a3e5a763107a71c397c4481d6a804eb5063d8bd", size = 191495, upload-time = "2025-10-02T14:35:12.971Z" }, + { url = "https://files.pythonhosted.org/packages/e9/3a/6797e0114c21d1725e2577508e24006fd7ff1d8c0c502d3b52e45c1771d8/xxhash-3.6.0-cp313-cp313-win32.whl", hash = "sha256:2577b276e060b73b73a53042ea5bd5203d3e6347ce0d09f98500f418a9fcf799", size = 30620, upload-time = "2025-10-02T14:35:14.129Z" }, + { url = "https://files.pythonhosted.org/packages/86/15/9bc32671e9a38b413a76d24722a2bf8784a132c043063a8f5152d390b0f9/xxhash-3.6.0-cp313-cp313-win_amd64.whl", hash = "sha256:757320d45d2fbcce8f30c42a6b2f47862967aea7bf458b9625b4bbe7ee390392", size = 31542, upload-time = "2025-10-02T14:35:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/39/c5/cc01e4f6188656e56112d6a8e0dfe298a16934b8c47a247236549a3f7695/xxhash-3.6.0-cp313-cp313-win_arm64.whl", hash = "sha256:457b8f85dec5825eed7b69c11ae86834a018b8e3df5e77783c999663da2f96d6", size = 27880, upload-time = "2025-10-02T14:35:16.315Z" }, + { url = "https://files.pythonhosted.org/packages/f3/30/25e5321c8732759e930c555176d37e24ab84365482d257c3b16362235212/xxhash-3.6.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a42e633d75cdad6d625434e3468126c73f13f7584545a9cf34e883aa1710e702", size = 32956, upload-time = "2025-10-02T14:35:17.413Z" }, + { url = "https://files.pythonhosted.org/packages/9f/3c/0573299560d7d9f8ab1838f1efc021a280b5ae5ae2e849034ef3dee18810/xxhash-3.6.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:568a6d743219e717b07b4e03b0a828ce593833e498c3b64752e0f5df6bfe84db", size = 31072, upload-time = "2025-10-02T14:35:18.844Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1c/52d83a06e417cd9d4137722693424885cc9878249beb3a7c829e74bf7ce9/xxhash-3.6.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bec91b562d8012dae276af8025a55811b875baace6af510412a5e58e3121bc54", size = 196409, upload-time = "2025-10-02T14:35:20.31Z" }, + { url = "https://files.pythonhosted.org/packages/e3/8e/c6d158d12a79bbd0b878f8355432075fc82759e356ab5a111463422a239b/xxhash-3.6.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78e7f2f4c521c30ad5e786fdd6bae89d47a32672a80195467b5de0480aa97b1f", size = 215736, upload-time = "2025-10-02T14:35:21.616Z" }, + { url = "https://files.pythonhosted.org/packages/bc/68/c4c80614716345d55071a396cf03d06e34b5f4917a467faf43083c995155/xxhash-3.6.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3ed0df1b11a79856df5ffcab572cbd6b9627034c1c748c5566fa79df9048a7c5", size = 214833, upload-time = "2025-10-02T14:35:23.32Z" }, + { url = "https://files.pythonhosted.org/packages/7e/e9/ae27c8ffec8b953efa84c7c4a6c6802c263d587b9fc0d6e7cea64e08c3af/xxhash-3.6.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0e4edbfc7d420925b0dd5e792478ed393d6e75ff8fc219a6546fb446b6a417b1", size = 448348, upload-time = "2025-10-02T14:35:25.111Z" }, + { url = "https://files.pythonhosted.org/packages/d7/6b/33e21afb1b5b3f46b74b6bd1913639066af218d704cc0941404ca717fc57/xxhash-3.6.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fba27a198363a7ef87f8c0f6b171ec36b674fe9053742c58dd7e3201c1ab30ee", size = 196070, upload-time = "2025-10-02T14:35:26.586Z" }, + { url = "https://files.pythonhosted.org/packages/96/b6/fcabd337bc5fa624e7203aa0fa7d0c49eed22f72e93229431752bddc83d9/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:794fe9145fe60191c6532fa95063765529770edcdd67b3d537793e8004cabbfd", size = 212907, upload-time = "2025-10-02T14:35:28.087Z" }, + { url = "https://files.pythonhosted.org/packages/4b/d3/9ee6160e644d660fcf176c5825e61411c7f62648728f69c79ba237250143/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:6105ef7e62b5ac73a837778efc331a591d8442f8ef5c7e102376506cb4ae2729", size = 200839, upload-time = "2025-10-02T14:35:29.857Z" }, + { url = "https://files.pythonhosted.org/packages/0d/98/e8de5baa5109394baf5118f5e72ab21a86387c4f89b0e77ef3e2f6b0327b/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:f01375c0e55395b814a679b3eea205db7919ac2af213f4a6682e01220e5fe292", size = 213304, upload-time = "2025-10-02T14:35:31.222Z" }, + { url = "https://files.pythonhosted.org/packages/7b/1d/71056535dec5c3177eeb53e38e3d367dd1d16e024e63b1cee208d572a033/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:d706dca2d24d834a4661619dcacf51a75c16d65985718d6a7d73c1eeeb903ddf", size = 416930, upload-time = "2025-10-02T14:35:32.517Z" }, + { url = "https://files.pythonhosted.org/packages/dc/6c/5cbde9de2cd967c322e651c65c543700b19e7ae3e0aae8ece3469bf9683d/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5f059d9faeacd49c0215d66f4056e1326c80503f51a1532ca336a385edadd033", size = 193787, upload-time = "2025-10-02T14:35:33.827Z" }, + { url = "https://files.pythonhosted.org/packages/19/fa/0172e350361d61febcea941b0cc541d6e6c8d65d153e85f850a7b256ff8a/xxhash-3.6.0-cp313-cp313t-win32.whl", hash = "sha256:1244460adc3a9be84731d72b8e80625788e5815b68da3da8b83f78115a40a7ec", size = 30916, upload-time = "2025-10-02T14:35:35.107Z" }, + { url = "https://files.pythonhosted.org/packages/ad/e6/e8cf858a2b19d6d45820f072eff1bea413910592ff17157cabc5f1227a16/xxhash-3.6.0-cp313-cp313t-win_amd64.whl", hash = "sha256:b1e420ef35c503869c4064f4a2f2b08ad6431ab7b229a05cce39d74268bca6b8", size = 31799, upload-time = "2025-10-02T14:35:36.165Z" }, + { url = "https://files.pythonhosted.org/packages/56/15/064b197e855bfb7b343210e82490ae672f8bc7cdf3ddb02e92f64304ee8a/xxhash-3.6.0-cp313-cp313t-win_arm64.whl", hash = "sha256:ec44b73a4220623235f67a996c862049f375df3b1052d9899f40a6382c32d746", size = 28044, upload-time = "2025-10-02T14:35:37.195Z" }, + { url = "https://files.pythonhosted.org/packages/7e/5e/0138bc4484ea9b897864d59fce9be9086030825bc778b76cb5a33a906d37/xxhash-3.6.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a40a3d35b204b7cc7643cbcf8c9976d818cb47befcfac8bbefec8038ac363f3e", size = 32754, upload-time = "2025-10-02T14:35:38.245Z" }, + { url = "https://files.pythonhosted.org/packages/18/d7/5dac2eb2ec75fd771957a13e5dda560efb2176d5203f39502a5fc571f899/xxhash-3.6.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a54844be970d3fc22630b32d515e79a90d0a3ddb2644d8d7402e3c4c8da61405", size = 30846, upload-time = "2025-10-02T14:35:39.6Z" }, + { url = "https://files.pythonhosted.org/packages/fe/71/8bc5be2bb00deb5682e92e8da955ebe5fa982da13a69da5a40a4c8db12fb/xxhash-3.6.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:016e9190af8f0a4e3741343777710e3d5717427f175adfdc3e72508f59e2a7f3", size = 194343, upload-time = "2025-10-02T14:35:40.69Z" }, + { url = "https://files.pythonhosted.org/packages/e7/3b/52badfb2aecec2c377ddf1ae75f55db3ba2d321c5e164f14461c90837ef3/xxhash-3.6.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4f6f72232f849eb9d0141e2ebe2677ece15adfd0fa599bc058aad83c714bb2c6", size = 213074, upload-time = "2025-10-02T14:35:42.29Z" }, + { url = "https://files.pythonhosted.org/packages/a2/2b/ae46b4e9b92e537fa30d03dbc19cdae57ed407e9c26d163895e968e3de85/xxhash-3.6.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:63275a8aba7865e44b1813d2177e0f5ea7eadad3dd063a21f7cf9afdc7054063", size = 212388, upload-time = "2025-10-02T14:35:43.929Z" }, + { url = "https://files.pythonhosted.org/packages/f5/80/49f88d3afc724b4ac7fbd664c8452d6db51b49915be48c6982659e0e7942/xxhash-3.6.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cd01fa2aa00d8b017c97eb46b9a794fbdca53fc14f845f5a328c71254b0abb7", size = 445614, upload-time = "2025-10-02T14:35:45.216Z" }, + { url = "https://files.pythonhosted.org/packages/ed/ba/603ce3961e339413543d8cd44f21f2c80e2a7c5cfe692a7b1f2cccf58f3c/xxhash-3.6.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0226aa89035b62b6a86d3c68df4d7c1f47a342b8683da2b60cedcddb46c4d95b", size = 194024, upload-time = "2025-10-02T14:35:46.959Z" }, + { url = "https://files.pythonhosted.org/packages/78/d1/8e225ff7113bf81545cfdcd79eef124a7b7064a0bba53605ff39590b95c2/xxhash-3.6.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c6e193e9f56e4ca4923c61238cdaced324f0feac782544eb4c6d55ad5cc99ddd", size = 210541, upload-time = "2025-10-02T14:35:48.301Z" }, + { url = "https://files.pythonhosted.org/packages/6f/58/0f89d149f0bad89def1a8dd38feb50ccdeb643d9797ec84707091d4cb494/xxhash-3.6.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9176dcaddf4ca963d4deb93866d739a343c01c969231dbe21680e13a5d1a5bf0", size = 198305, upload-time = "2025-10-02T14:35:49.584Z" }, + { url = "https://files.pythonhosted.org/packages/11/38/5eab81580703c4df93feb5f32ff8fa7fe1e2c51c1f183ee4e48d4bb9d3d7/xxhash-3.6.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c1ce4009c97a752e682b897aa99aef84191077a9433eb237774689f14f8ec152", size = 210848, upload-time = "2025-10-02T14:35:50.877Z" }, + { url = "https://files.pythonhosted.org/packages/5e/6b/953dc4b05c3ce678abca756416e4c130d2382f877a9c30a20d08ee6a77c0/xxhash-3.6.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:8cb2f4f679b01513b7adbb9b1b2f0f9cdc31b70007eaf9d59d0878809f385b11", size = 414142, upload-time = "2025-10-02T14:35:52.15Z" }, + { url = "https://files.pythonhosted.org/packages/08/a9/238ec0d4e81a10eb5026d4a6972677cbc898ba6c8b9dbaec12ae001b1b35/xxhash-3.6.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:653a91d7c2ab54a92c19ccf43508b6a555440b9be1bc8be553376778be7f20b5", size = 191547, upload-time = "2025-10-02T14:35:53.547Z" }, + { url = "https://files.pythonhosted.org/packages/f1/ee/3cf8589e06c2164ac77c3bf0aa127012801128f1feebf2a079272da5737c/xxhash-3.6.0-cp314-cp314-win32.whl", hash = "sha256:a756fe893389483ee8c394d06b5ab765d96e68fbbfe6fde7aa17e11f5720559f", size = 31214, upload-time = "2025-10-02T14:35:54.746Z" }, + { url = "https://files.pythonhosted.org/packages/02/5d/a19552fbc6ad4cb54ff953c3908bbc095f4a921bc569433d791f755186f1/xxhash-3.6.0-cp314-cp314-win_amd64.whl", hash = "sha256:39be8e4e142550ef69629c9cd71b88c90e9a5db703fecbcf265546d9536ca4ad", size = 32290, upload-time = "2025-10-02T14:35:55.791Z" }, + { url = "https://files.pythonhosted.org/packages/b1/11/dafa0643bc30442c887b55baf8e73353a344ee89c1901b5a5c54a6c17d39/xxhash-3.6.0-cp314-cp314-win_arm64.whl", hash = "sha256:25915e6000338999236f1eb68a02a32c3275ac338628a7eaa5a269c401995679", size = 28795, upload-time = "2025-10-02T14:35:57.162Z" }, + { url = "https://files.pythonhosted.org/packages/2c/db/0e99732ed7f64182aef4a6fb145e1a295558deec2a746265dcdec12d191e/xxhash-3.6.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c5294f596a9017ca5a3e3f8884c00b91ab2ad2933cf288f4923c3fd4346cf3d4", size = 32955, upload-time = "2025-10-02T14:35:58.267Z" }, + { url = "https://files.pythonhosted.org/packages/55/f4/2a7c3c68e564a099becfa44bb3d398810cc0ff6749b0d3cb8ccb93f23c14/xxhash-3.6.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1cf9dcc4ab9cff01dfbba78544297a3a01dafd60f3bde4e2bfd016cf7e4ddc67", size = 31072, upload-time = "2025-10-02T14:35:59.382Z" }, + { url = "https://files.pythonhosted.org/packages/c6/d9/72a29cddc7250e8a5819dad5d466facb5dc4c802ce120645630149127e73/xxhash-3.6.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:01262da8798422d0685f7cef03b2bd3f4f46511b02830861df548d7def4402ad", size = 196579, upload-time = "2025-10-02T14:36:00.838Z" }, + { url = "https://files.pythonhosted.org/packages/63/93/b21590e1e381040e2ca305a884d89e1c345b347404f7780f07f2cdd47ef4/xxhash-3.6.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51a73fb7cb3a3ead9f7a8b583ffd9b8038e277cdb8cb87cf890e88b3456afa0b", size = 215854, upload-time = "2025-10-02T14:36:02.207Z" }, + { url = "https://files.pythonhosted.org/packages/ce/b8/edab8a7d4fa14e924b29be877d54155dcbd8b80be85ea00d2be3413a9ed4/xxhash-3.6.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b9c6df83594f7df8f7f708ce5ebeacfc69f72c9fbaaababf6cf4758eaada0c9b", size = 214965, upload-time = "2025-10-02T14:36:03.507Z" }, + { url = "https://files.pythonhosted.org/packages/27/67/dfa980ac7f0d509d54ea0d5a486d2bb4b80c3f1bb22b66e6a05d3efaf6c0/xxhash-3.6.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:627f0af069b0ea56f312fd5189001c24578868643203bca1abbc2c52d3a6f3ca", size = 448484, upload-time = "2025-10-02T14:36:04.828Z" }, + { url = "https://files.pythonhosted.org/packages/8c/63/8ffc2cc97e811c0ca5d00ab36604b3ea6f4254f20b7bc658ca825ce6c954/xxhash-3.6.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aa912c62f842dfd013c5f21a642c9c10cd9f4c4e943e0af83618b4a404d9091a", size = 196162, upload-time = "2025-10-02T14:36:06.182Z" }, + { url = "https://files.pythonhosted.org/packages/4b/77/07f0e7a3edd11a6097e990f6e5b815b6592459cb16dae990d967693e6ea9/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b465afd7909db30168ab62afe40b2fcf79eedc0b89a6c0ab3123515dc0df8b99", size = 213007, upload-time = "2025-10-02T14:36:07.733Z" }, + { url = "https://files.pythonhosted.org/packages/ae/d8/bc5fa0d152837117eb0bef6f83f956c509332ce133c91c63ce07ee7c4873/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:a881851cf38b0a70e7c4d3ce81fc7afd86fbc2a024f4cfb2a97cf49ce04b75d3", size = 200956, upload-time = "2025-10-02T14:36:09.106Z" }, + { url = "https://files.pythonhosted.org/packages/26/a5/d749334130de9411783873e9b98ecc46688dad5db64ca6e04b02acc8b473/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:9b3222c686a919a0f3253cfc12bb118b8b103506612253b5baeaac10d8027cf6", size = 213401, upload-time = "2025-10-02T14:36:10.585Z" }, + { url = "https://files.pythonhosted.org/packages/89/72/abed959c956a4bfc72b58c0384bb7940663c678127538634d896b1195c10/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:c5aa639bc113e9286137cec8fadc20e9cd732b2cc385c0b7fa673b84fc1f2a93", size = 417083, upload-time = "2025-10-02T14:36:12.276Z" }, + { url = "https://files.pythonhosted.org/packages/0c/b3/62fd2b586283b7d7d665fb98e266decadf31f058f1cf6c478741f68af0cb/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5c1343d49ac102799905e115aee590183c3921d475356cb24b4de29a4bc56518", size = 193913, upload-time = "2025-10-02T14:36:14.025Z" }, + { url = "https://files.pythonhosted.org/packages/9a/9a/c19c42c5b3f5a4aad748a6d5b4f23df3bed7ee5445accc65a0fb3ff03953/xxhash-3.6.0-cp314-cp314t-win32.whl", hash = "sha256:5851f033c3030dd95c086b4a36a2683c2ff4a799b23af60977188b057e467119", size = 31586, upload-time = "2025-10-02T14:36:15.603Z" }, + { url = "https://files.pythonhosted.org/packages/03/d6/4cc450345be9924fd5dc8c590ceda1db5b43a0a889587b0ae81a95511360/xxhash-3.6.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0444e7967dac37569052d2409b00a8860c2135cff05502df4da80267d384849f", size = 32526, upload-time = "2025-10-02T14:36:16.708Z" }, + { url = "https://files.pythonhosted.org/packages/0f/c9/7243eb3f9eaabd1a88a5a5acadf06df2d83b100c62684b7425c6a11bcaa8/xxhash-3.6.0-cp314-cp314t-win_arm64.whl", hash = "sha256:bb79b1e63f6fd84ec778a4b1916dfe0a7c3fdb986c06addd5db3a0d413819d95", size = 28898, upload-time = "2025-10-02T14:36:17.843Z" }, + { url = "https://files.pythonhosted.org/packages/93/1e/8aec23647a34a249f62e2398c42955acd9b4c6ed5cf08cbea94dc46f78d2/xxhash-3.6.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0f7b7e2ec26c1666ad5fc9dbfa426a6a3367ceaf79db5dd76264659d509d73b0", size = 30662, upload-time = "2025-10-02T14:37:01.743Z" }, + { url = "https://files.pythonhosted.org/packages/b8/0b/b14510b38ba91caf43006209db846a696ceea6a847a0c9ba0a5b1adc53d6/xxhash-3.6.0-pp311-pypy311_pp73-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5dc1e14d14fa0f5789ec29a7062004b5933964bb9b02aae6622b8f530dc40296", size = 41056, upload-time = "2025-10-02T14:37:02.879Z" }, + { url = "https://files.pythonhosted.org/packages/50/55/15a7b8a56590e66ccd374bbfa3f9ffc45b810886c8c3b614e3f90bd2367c/xxhash-3.6.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:881b47fc47e051b37d94d13e7455131054b56749b91b508b0907eb07900d1c13", size = 36251, upload-time = "2025-10-02T14:37:04.44Z" }, + { url = "https://files.pythonhosted.org/packages/62/b2/5ac99a041a29e58e95f907876b04f7067a0242cb85b5f39e726153981503/xxhash-3.6.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c6dc31591899f5e5666f04cc2e529e69b4072827085c1ef15294d91a004bc1bd", size = 32481, upload-time = "2025-10-02T14:37:05.869Z" }, + { url = "https://files.pythonhosted.org/packages/7b/d9/8d95e906764a386a3d3b596f3c68bb63687dfca806373509f51ce8eea81f/xxhash-3.6.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:15e0dac10eb9309508bfc41f7f9deaa7755c69e35af835db9cb10751adebc35d", size = 31565, upload-time = "2025-10-02T14:37:06.966Z" }, +] diff --git a/sample_projects/16_depends_on/main.netrun.json b/sample_projects/16_depends_on/main.netrun.json new file mode 100644 index 00000000..7d3ec53e --- /dev/null +++ b/sample_projects/16_depends_on/main.netrun.json @@ -0,0 +1,43 @@ +{ + "pools": { + "main": {"spec": {"type": "main"}} + }, + "output_queues": { + "all_done": { + "ports": [ + ["step_a", "out"], + ["step_b", "out"], + ["step_c", "out"] + ] + } + }, + "graph": { + "nodes": [ + { + "name": "step_a", + "factory": "netrun.node_factories.from_function", + "factory_args": {"func": "nodes.step_a"}, + "execution_config": {"pools": ["main"]} + }, + { + "name": "step_b", + "factory": "netrun.node_factories.from_function", + "factory_args": {"func": "nodes.step_b"}, + "execution_config": { + "pools": ["main"], + "depends_on": ["step_a"] + } + }, + { + "name": "step_c", + "factory": "netrun.node_factories.from_function", + "factory_args": {"func": "nodes.step_c"}, + "execution_config": { + "pools": ["main"], + "depends_on": ["step_b"] + } + } + ], + "edges": [] + } +} diff --git a/sample_projects/16_depends_on/main.py b/sample_projects/16_depends_on/main.py new file mode 100644 index 00000000..fea370aa --- /dev/null +++ b/sample_projects/16_depends_on/main.py @@ -0,0 +1,28 @@ +"""Run the depends_on sample. + +Three nodes A, B, C with no data edges between them. The depends_on field +forces A → B → C ordering. Watch the output queue: results should appear +in order regardless of injection order. +""" + +import asyncio +from pathlib import Path + +from netrun.core import Net, NetConfig + + +async def main(): + config = NetConfig.from_file(Path(__file__).parent / "main.netrun.json") + async with Net(config) as net: + # Inject all triggers up front. depends_on enforces the order. + net.inject_data("step_a", "trigger", ["go"]) + net.inject_data("step_b", "trigger", ["go"]) + net.inject_data("step_c", "trigger", ["go"]) + + await net.run_until_blocked() + + print("\nCompletion order:", net.flush_output_queue("all_done")) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/sample_projects/16_depends_on/nodes.py b/sample_projects/16_depends_on/nodes.py new file mode 100644 index 00000000..0e92ab20 --- /dev/null +++ b/sample_projects/16_depends_on/nodes.py @@ -0,0 +1,32 @@ +"""Three independent nodes ordered with depends_on. + +There are NO data edges between A, B, C — the only thing forcing them to +run in order A → B → C is the `depends_on` field on each node's +execution_config. + +Each node simulates a side-effecting setup step (e.g. provisioning, +schema migration, warmup) where ordering matters but data doesn't flow. +""" + +import time + + +def step_a(trigger, print) -> str: + print("step_a: starting (no dependencies)") + time.sleep(0.05) + print("step_a: done") + return "A complete" + + +def step_b(trigger, print) -> str: + print("step_b: starting (depends on A)") + time.sleep(0.05) + print("step_b: done") + return "B complete" + + +def step_c(trigger, print) -> str: + print("step_c: starting (depends on B)") + time.sleep(0.05) + print("step_c: done") + return "C complete" diff --git a/sample_projects/16_depends_on/pyproject.toml b/sample_projects/16_depends_on/pyproject.toml new file mode 100644 index 00000000..b1687082 --- /dev/null +++ b/sample_projects/16_depends_on/pyproject.toml @@ -0,0 +1,9 @@ +[project] +name = "depends-on-sample" +version = "0.1.0" +description = "Demonstrates depends_on for node ordering without data edges" +requires-python = ">=3.11" +dependencies = ["netrun"] + +[tool.uv.sources] +netrun = { path = "../../netrun", editable = true } diff --git a/sample_projects/16_depends_on/uv.lock b/sample_projects/16_depends_on/uv.lock new file mode 100644 index 00000000..70897a91 --- /dev/null +++ b/sample_projects/16_depends_on/uv.lock @@ -0,0 +1,1235 @@ +version = 1 +revision = 3 +requires-python = ">=3.11" + +[options] +exclude-newer = "2026-04-24T11:32:47.180411Z" +exclude-newer-span = "P7D" + +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "attrs" +version = "26.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, +] + +[[package]] +name = "beartype" +version = "0.22.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/94/1009e248bbfbab11397abca7193bea6626806be9a327d399810d523a07cb/beartype-0.22.9.tar.gz", hash = "sha256:8f82b54aa723a2848a56008d18875f91c1db02c32ef6a62319a002e3e25a975f", size = 1608866, upload-time = "2025-12-13T06:50:30.72Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/cc/18245721fa7747065ab478316c7fea7c74777d07f37ae60db2e84f8172e8/beartype-0.22.9-py3-none-any.whl", hash = "sha256:d16c9bbc61ea14637596c5f6fbff2ee99cbe3573e46a716401734ef50c3060c2", size = 1333658, upload-time = "2025-12-13T06:50:28.266Z" }, +] + +[[package]] +name = "beautifulsoup4" +version = "4.14.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "soupsieve" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c3/b0/1c6a16426d389813b48d95e26898aff79abbde42ad353958ad95cc8c9b21/beautifulsoup4-4.14.3.tar.gz", hash = "sha256:6292b1c5186d356bba669ef9f7f051757099565ad9ada5dd630bd9de5fa7fb86", size = 627737, upload-time = "2025-11-30T15:08:26.084Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-py3-none-any.whl", hash = "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb", size = 107721, upload-time = "2025-11-30T15:08:24.087Z" }, +] + +[[package]] +name = "bleach" +version = "6.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "webencodings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/07/18/3c8523962314be6bf4c8989c79ad9531c825210dd13a8669f6b84336e8bd/bleach-6.3.0.tar.gz", hash = "sha256:6f3b91b1c0a02bb9a78b5a454c92506aa0fdf197e1d5e114d2e00c6f64306d22", size = 203533, upload-time = "2025-10-27T17:57:39.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cd/3a/577b549de0cc09d95f11087ee63c739bba856cd3952697eec4c4bb91350a/bleach-6.3.0-py3-none-any.whl", hash = "sha256:fe10ec77c93ddf3d13a73b035abaac7a9f5e436513864ccdad516693213c65d6", size = 164437, upload-time = "2025-10-27T17:57:37.538Z" }, +] + +[package.optional-dependencies] +css = [ + { name = "tinycss2" }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, + { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, + { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, + { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, + { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, + { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, + { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, + { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, + { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, +] + +[[package]] +name = "click" +version = "8.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/63/f9e1ea081ce35720d8b92acde70daaedace594dc93b693c869e0d5910718/click-8.3.3.tar.gz", hash = "sha256:398329ad4837b2ff7cbe1dd166a4c0f8900c3ca3a218de04466f38f6497f18a2", size = 328061, upload-time = "2026-04-22T15:11:27.506Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl", hash = "sha256:a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613", size = 110502, upload-time = "2026-04-22T15:11:25.044Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "defusedxml" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520, upload-time = "2021-03-08T10:59:26.269Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" }, +] + +[[package]] +name = "depends-on-sample" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "netrun" }, +] + +[package.metadata] +requires-dist = [{ name = "netrun", editable = "../../netrun" }] + +[[package]] +name = "diskcache" +version = "5.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3f/21/1c1ffc1a039ddcc459db43cc108658f32c57d271d7289a2794e401d0fdb6/diskcache-5.6.3.tar.gz", hash = "sha256:2c3a3fa2743d8535d832ec61c2054a1641f41775aa7c556758a109941e33e4fc", size = 67916, upload-time = "2023-08-31T06:12:00.316Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/27/4570e78fc0bf5ea0ca45eb1de3818a23787af9b390c0b0a0033a1b8236f9/diskcache-5.6.3-py3-none-any.whl", hash = "sha256:5e31b2d5fbad117cc363ebaf6b689474db18a1f6438bc82358b024abd4c2ca19", size = 45550, upload-time = "2023-08-31T06:11:58.822Z" }, +] + +[[package]] +name = "fastjsonschema" +version = "2.21.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/b5/23b216d9d985a956623b6bd12d4086b60f0059b27799f23016af04a74ea1/fastjsonschema-2.21.2.tar.gz", hash = "sha256:b1eb43748041c880796cd077f1a07c3d94e93ae84bba5ed36800a33554ae05de", size = 374130, upload-time = "2025-08-14T18:49:36.666Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/a8/20d0723294217e47de6d9e2e40fd4a9d2f7c4b6ef974babd482a59743694/fastjsonschema-2.21.2-py3-none-any.whl", hash = "sha256:1c797122d0a86c5cace2e54bf4e819c36223b552017172f32c5c024a6b77e463", size = 24024, upload-time = "2025-08-14T18:49:34.776Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "jsonschema" +version = "4.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, +] + +[[package]] +name = "jupyter-client" +version = "8.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jupyter-core" }, + { name = "python-dateutil" }, + { name = "pyzmq" }, + { name = "tornado" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/05/e4/ba649102a3bc3fbca54e7239fb924fd434c766f855693d86de0b1f2bec81/jupyter_client-8.8.0.tar.gz", hash = "sha256:d556811419a4f2d96c869af34e854e3f059b7cc2d6d01a9cd9c85c267691be3e", size = 348020, upload-time = "2026-01-08T13:55:47.938Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/0b/ceb7694d864abc0a047649aec263878acb9f792e1fec3e676f22dc9015e3/jupyter_client-8.8.0-py3-none-any.whl", hash = "sha256:f93a5b99c5e23a507b773d3a1136bd6e16c67883ccdbd9a829b0bbdb98cd7d7a", size = 107371, upload-time = "2026-01-08T13:55:45.562Z" }, +] + +[[package]] +name = "jupyter-core" +version = "5.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "platformdirs" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/49/9d1284d0dc65e2c757b74c6687b6d319b02f822ad039e5c512df9194d9dd/jupyter_core-5.9.1.tar.gz", hash = "sha256:4d09aaff303b9566c3ce657f580bd089ff5c91f5f89cf7d8846c3cdf465b5508", size = 89814, upload-time = "2025-10-16T19:19:18.444Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl", hash = "sha256:ebf87fdc6073d142e114c72c9e29a9d7ca03fad818c5d300ce2adc1fb0743407", size = 29032, upload-time = "2025-10-16T19:19:16.783Z" }, +] + +[[package]] +name = "jupyterlab-pygments" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/90/51/9187be60d989df97f5f0aba133fa54e7300f17616e065d1ada7d7646b6d6/jupyterlab_pygments-0.3.0.tar.gz", hash = "sha256:721aca4d9029252b11cfa9d185e5b5af4d54772bb8072f9b7036f4170054d35d", size = 512900, upload-time = "2023-11-23T09:26:37.44Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/dd/ead9d8ea85bf202d90cc513b533f9c363121c7792674f78e0d8a854b63b4/jupyterlab_pygments-0.3.0-py3-none-any.whl", hash = "sha256:841a89020971da1d8693f1a99997aefc5dc424bb1b251fd6322462a1b8842780", size = 15884, upload-time = "2023-11-23T09:26:34.325Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, + { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, + { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, + { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, + { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, + { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "mistune" +version = "3.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/55/d01f0c4b45ade6536c51170b9043db8b2ec6ddf4a35c7ea3f5f559ac935b/mistune-3.2.0.tar.gz", hash = "sha256:708487c8a8cdd99c9d90eb3ed4c3ed961246ff78ac82f03418f5183ab70e398a", size = 95467, upload-time = "2025-12-23T11:36:34.994Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/f7/4a5e785ec9fbd65146a27b6b70b6cdc161a66f2024e4b04ac06a67f5578b/mistune-3.2.0-py3-none-any.whl", hash = "sha256:febdc629a3c78616b94393c6580551e0e34cc289987ec6c35ed3f4be42d0eee1", size = 53598, upload-time = "2025-12-23T11:36:33.211Z" }, +] + +[[package]] +name = "nbclient" +version = "0.10.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jupyter-client" }, + { name = "jupyter-core" }, + { name = "nbformat" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/56/91/1c1d5a4b9a9ebba2b4e32b8c852c2975c872aec1fe42ab5e516b2cecd193/nbclient-0.10.4.tar.gz", hash = "sha256:1e54091b16e6da39e297b0ece3e10f6f29f4ac4e8ee515d29f8a7099bd6553c9", size = 62554, upload-time = "2025-12-23T07:45:46.369Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/a0/5b0c2f11142ed1dddec842457d3f65eaf71a0080894eb6f018755b319c3a/nbclient-0.10.4-py3-none-any.whl", hash = "sha256:9162df5a7373d70d606527300a95a975a47c137776cd942e52d9c7e29ff83440", size = 25465, upload-time = "2025-12-23T07:45:44.51Z" }, +] + +[[package]] +name = "nbconvert" +version = "7.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beautifulsoup4" }, + { name = "bleach", extra = ["css"] }, + { name = "defusedxml" }, + { name = "jinja2" }, + { name = "jupyter-core" }, + { name = "jupyterlab-pygments" }, + { name = "markupsafe" }, + { name = "mistune" }, + { name = "nbclient" }, + { name = "nbformat" }, + { name = "packaging" }, + { name = "pandocfilters" }, + { name = "pygments" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/01/b1/708e53fe2e429c103c6e6e159106bcf0357ac41aa4c28772bd8402339051/nbconvert-7.17.1.tar.gz", hash = "sha256:34d0d0a7e73ce3cbab6c5aae8f4f468797280b01fd8bd2ca746da8569eddd7d2", size = 865311, upload-time = "2026-04-08T00:44:14.914Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/f8/bb0a9d5f46819c821dc1f004aa2cc29b1d91453297dbf5ff20470f00f193/nbconvert-7.17.1-py3-none-any.whl", hash = "sha256:aa85c087b435e7bf1ffd03319f658e285f2b89eccab33bc1ba7025495ab3e7c8", size = 261927, upload-time = "2026-04-08T00:44:12.845Z" }, +] + +[[package]] +name = "nbformat" +version = "5.10.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fastjsonschema" }, + { name = "jsonschema" }, + { name = "jupyter-core" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/fd/91545e604bc3dad7dca9ed03284086039b294c6b3d75c0d2fa45f9e9caf3/nbformat-5.10.4.tar.gz", hash = "sha256:322168b14f937a5d11362988ecac2a4952d3d8e3a2cbeb2319584631226d5b3a", size = 142749, upload-time = "2024-04-04T11:20:37.371Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/82/0340caa499416c78e5d8f5f05947ae4bc3cba53c9f038ab6e9ed964e22f1/nbformat-5.10.4-py3-none-any.whl", hash = "sha256:3b48d6c8fbca4b299bf3982ea7db1af21580e4fec269ad087b9e81588891200b", size = 78454, upload-time = "2024-04-04T11:20:34.895Z" }, +] + +[[package]] +name = "nblite" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jinja2" }, + { name = "nbconvert" }, + { name = "nbformat" }, + { name = "notebookx-py" }, + { name = "pydantic" }, + { name = "pyyaml" }, + { name = "rich" }, + { name = "typer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/80/a5/e7a91b57e1dc17d0e546e95500388b957320588a563c20c053eb593b3353/nblite-1.2.1.tar.gz", hash = "sha256:0f5b129d93069f0f7502ddcc1f8d960af78eefb9d097b1b85bad088ec1618cbb", size = 325768, upload-time = "2026-03-15T10:50:07.001Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/4d/471d79351bab69bf96f12d3b689d430342857138bb45fb82cb08a19125d1/nblite-1.2.1-py3-none-any.whl", hash = "sha256:04083f525229f8d8445c08dc192286491ec63e730806c84faf226b9d33b35ca0", size = 105264, upload-time = "2026-03-15T10:50:05.235Z" }, +] + +[[package]] +name = "netrun" +version = "0.5.0" +source = { editable = "../../netrun" } +dependencies = [ + { name = "beartype" }, + { name = "diskcache" }, + { name = "nblite" }, + { name = "netrun-sim" }, + { name = "websockets" }, + { name = "xxhash" }, +] + +[package.metadata] +requires-dist = [ + { name = "beartype", specifier = ">=0.22.9" }, + { name = "cloudpickle", marker = "extra == 'serialization'", specifier = ">=3.0" }, + { name = "dill", marker = "extra == 'serialization'", specifier = ">=0.3" }, + { name = "diskcache", specifier = ">=5.6.3" }, + { name = "nblite", specifier = ">=1.1.1" }, + { name = "netrun-sim", editable = "../../netrun-sim/python" }, + { name = "pyinfra", marker = "extra == 'deploy'", specifier = ">=3.0" }, + { name = "websockets", specifier = ">=16.0" }, + { name = "xxhash", specifier = ">=3.6.0" }, +] +provides-extras = ["deploy", "serialization"] + +[package.metadata.requires-dev] +dev = [ + { name = "dev", editable = "../../netrun/src/dev" }, + { name = "jupyterlab", specifier = ">=4.5.1" }, + { name = "mypy", specifier = ">=1.19.1" }, + { name = "nblite", specifier = ">=1.1.5" }, + { name = "netrun-cli", editable = "../../netrun-cli" }, + { name = "pytest", specifier = ">=9.0.2" }, + { name = "pytest-asyncio", specifier = ">=1.3.0" }, + { name = "ruff", specifier = ">=0.8.0" }, +] + +[[package]] +name = "netrun-sim" +version = "0.2.0" +source = { editable = "../../netrun-sim/python" } +dependencies = [ + { name = "python-ulid" }, +] + +[package.metadata] +requires-dist = [{ name = "python-ulid", specifier = ">=1.0" }] + +[package.metadata.requires-dev] +dev = [ + { name = "git-cliff", specifier = ">=2.11.0" }, + { name = "jupyterlab", specifier = ">=4.3.8" }, + { name = "maturin", specifier = ">=1.10.2" }, + { name = "nblite", specifier = ">=1.1.1" }, + { name = "python-ulid", specifier = ">=1.1.0" }, + { name = "ruff", specifier = ">=0.14.11" }, + { name = "twine", specifier = ">=6.1.0" }, + { name = "typing-extensions", specifier = ">=4.13.2" }, +] + +[[package]] +name = "notebookx-py" +version = "0.1.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/8f/a052edfbddf51d4ec485bf05ec0b526fc9d4d476cbdea01b60a040b4dd4c/notebookx_py-0.1.8.tar.gz", hash = "sha256:cee26716a9436b7ee0771ec840c49d268068d1e7688b9a4d3da79c763cd07a11", size = 40139, upload-time = "2026-01-22T19:31:59.907Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/24/f1d34cf2dca8ad9c008345d6900460991535e9b708979a39e6c6df8580a6/notebookx_py-0.1.8-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:e30a0677f752acaa47bbee12f00b6a3a9caf48e1b10637699dc956c99dbcf17e", size = 762017, upload-time = "2026-01-22T19:31:48.983Z" }, + { url = "https://files.pythonhosted.org/packages/54/78/a43c065515b8a26a36e1b1ce96f15b166e4c5beb4d026317adc6db86074f/notebookx_py-0.1.8-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:6adfd281c4c4c4a6dea7ae9e9c5ed4e04359a1425a78d9e33acdd3914c579752", size = 732635, upload-time = "2026-01-22T19:31:51.027Z" }, + { url = "https://files.pythonhosted.org/packages/89/ed/7141c4baf3ef1d0ee80bd18b0c6c5b1ee45d6320c86c5c850fd2ab1d8d35/notebookx_py-0.1.8-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ae180b4ef7939b4567ca97e60c23acac79e5c17d66ccbca51f1b4faa72ed35a", size = 782241, upload-time = "2026-01-22T19:31:52.379Z" }, + { url = "https://files.pythonhosted.org/packages/ba/f3/b97946c846113f86537e202679d3322e06f5014d86e06fe061767d1cafd2/notebookx_py-0.1.8-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d34febdababaec0b5fca2355d3dbd6f69c3b0b596b26daec5ffa66c754bda94", size = 806947, upload-time = "2026-01-22T19:31:54.329Z" }, + { url = "https://files.pythonhosted.org/packages/07/0d/266611c951bea05711a2bf4791b035720a9daec1181d46589acc57d9f38c/notebookx_py-0.1.8-cp38-abi3-win_amd64.whl", hash = "sha256:fa4d013e4272d3ae714d5ca2206db9d263f85064ecb7380ee7bf722dee5d4625", size = 624603, upload-time = "2026-01-22T19:31:55.687Z" }, +] + +[[package]] +name = "packaging" +version = "26.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/df/de/0d2b39fb4af88a0258f3bac87dfcbb48e73fbdea4a2ed0e2213f9a4c2f9a/packaging-26.1.tar.gz", hash = "sha256:f042152b681c4bfac5cae2742a55e103d27ab2ec0f3d88037136b6bfe7c9c5de", size = 215519, upload-time = "2026-04-14T21:12:49.362Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/c2/920ef838e2f0028c8262f16101ec09ebd5969864e5a64c4c05fad0617c56/packaging-26.1-py3-none-any.whl", hash = "sha256:5d9c0669c6285e491e0ced2eee587eaf67b670d94a19e94e3984a481aba6802f", size = 95831, upload-time = "2026-04-14T21:12:47.56Z" }, +] + +[[package]] +name = "pandocfilters" +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/70/6f/3dd4940bbe001c06a65f88e36bad298bc7a0de5036115639926b0c5c0458/pandocfilters-1.5.1.tar.gz", hash = "sha256:002b4a555ee4ebc03f8b66307e287fa492e4a77b4ea14d3f934328297bb4939e", size = 8454, upload-time = "2024-01-18T20:08:13.726Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/af/4fbc8cab944db5d21b7e2a5b8e9211a03a79852b1157e2c102fcc61ac440/pandocfilters-1.5.1-py2.py3-none-any.whl", hash = "sha256:93be382804a9cdb0a7267585f157e5d1731bbe5545a85b268d6f5fe6232de2bc", size = 8663, upload-time = "2024-01-18T20:08:11.28Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.9.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9f/4a/0883b8e3802965322523f0b200ecf33d31f10991d0401162f4b23c698b42/platformdirs-4.9.6.tar.gz", hash = "sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a", size = 29400, upload-time = "2026-04-09T00:04:10.812Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/a6/a0a304dc33b49145b21f4808d763822111e67d1c3a32b524a1baf947b6e1/platformdirs-4.9.6-py3-none-any.whl", hash = "sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917", size = 21348, upload-time = "2026-04-09T00:04:09.463Z" }, +] + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + +[[package]] +name = "pydantic" +version = "2.13.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d9/e4/40d09941a2cebcb20609b86a559817d5b9291c49dd6f8c87e5feffbe703a/pydantic-2.13.3.tar.gz", hash = "sha256:af09e9d1d09f4e7fe37145c1f577e1d61ceb9a41924bf0094a36506285d0a84d", size = 844068, upload-time = "2026-04-20T14:46:43.632Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/0a/fd7d723f8f8153418fb40cf9c940e82004fce7e987026b08a68a36dd3fe7/pydantic-2.13.3-py3-none-any.whl", hash = "sha256:6db14ac8dfc9a1e57f87ea2c0de670c251240f43cb0c30a5130e9720dc612927", size = 471981, upload-time = "2026-04-20T14:46:41.402Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.46.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2a/ef/f7abb56c49382a246fd2ce9c799691e3c3e7175ec74b14d99e798bcddb1a/pydantic_core-2.46.3.tar.gz", hash = "sha256:41c178f65b8c29807239d47e6050262eb6bf84eb695e41101e62e38df4a5bc2c", size = 471412, upload-time = "2026-04-20T14:40:56.672Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/a2/1ba90a83e85a3f94c796b184f3efde9c72f2830dcda493eea8d59ba78e6d/pydantic_core-2.46.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ab124d49d0459b2373ecf54118a45c28a1e6d4192a533fbc915e70f556feb8e5", size = 2106740, upload-time = "2026-04-20T14:41:20.932Z" }, + { url = "https://files.pythonhosted.org/packages/b6/f6/99ae893c89a0b9d3daec9f95487aa676709aa83f67643b3f0abaf4ab628a/pydantic_core-2.46.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cca67d52a5c7a16aed2b3999e719c4bcf644074eac304a5d3d62dd70ae7d4b2c", size = 1948293, upload-time = "2026-04-20T14:43:42.115Z" }, + { url = "https://files.pythonhosted.org/packages/3e/b8/2e8e636dc9e3f16c2e16bf0849e24be82c5ee82c603c65fc0326666328fc/pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c024e08c0ba23e6fd68c771a521e9d6a792f2ebb0fa734296b36394dc30390e", size = 1973222, upload-time = "2026-04-20T14:41:57.841Z" }, + { url = "https://files.pythonhosted.org/packages/34/36/0e730beec4d83c5306f417afbd82ff237d9a21e83c5edf675f31ed84c1fe/pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6645ce7eec4928e29a1e3b3d5c946621d105d3e79f0c9cddf07c2a9770949287", size = 2053852, upload-time = "2026-04-20T14:40:43.077Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f0/3071131f47e39136a17814576e0fada9168569f7f8c0e6ac4d1ede6a4958/pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a712c7118e6c5ea96562f7b488435172abb94a3c53c22c9efc1412264a45cbbe", size = 2221134, upload-time = "2026-04-20T14:43:03.349Z" }, + { url = "https://files.pythonhosted.org/packages/2f/a9/a2dc023eec5aa4b02a467874bad32e2446957d2adcab14e107eab502e978/pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:69a868ef3ff206343579021c40faf3b1edc64b1cc508ff243a28b0a514ccb050", size = 2279785, upload-time = "2026-04-20T14:41:19.285Z" }, + { url = "https://files.pythonhosted.org/packages/0a/44/93f489d16fb63fbd41c670441536541f6e8cfa1e5a69f40bc9c5d30d8c90/pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc7e8c32db809aa0f6ea1d6869ebc8518a65d5150fdfad8bcae6a49ae32a22e2", size = 2089404, upload-time = "2026-04-20T14:43:10.108Z" }, + { url = "https://files.pythonhosted.org/packages/2a/78/8692e3aa72b2d004f7a5d937f1dfdc8552ba26caf0bec75f342c40f00dec/pydantic_core-2.46.3-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:3481bd1341dc85779ee506bc8e1196a277ace359d89d28588a9468c3ecbe63fa", size = 2114898, upload-time = "2026-04-20T14:44:51.475Z" }, + { url = "https://files.pythonhosted.org/packages/6a/62/e83133f2e7832532060175cebf1f13748f4c7e7e7165cdd1f611f174494b/pydantic_core-2.46.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8690eba565c6d68ffd3a8655525cbdd5246510b44a637ee2c6c03a7ebfe64d3c", size = 2157856, upload-time = "2026-04-20T14:43:46.64Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ec/6a500e3ad7718ee50583fae79c8651f5d37e3abce1fa9ae177ae65842c53/pydantic_core-2.46.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4de88889d7e88d50d40ee5b39d5dac0bcaef9ba91f7e536ac064e6b2834ecccf", size = 2180168, upload-time = "2026-04-20T14:42:00.302Z" }, + { url = "https://files.pythonhosted.org/packages/d8/53/8267811054b1aa7fc1dc7ded93812372ef79a839f5e23558136a6afbfde1/pydantic_core-2.46.3-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:e480080975c1ef7f780b8f99ed72337e7cc5efea2e518a20a692e8e7b278eb8b", size = 2322885, upload-time = "2026-04-20T14:41:05.253Z" }, + { url = "https://files.pythonhosted.org/packages/c8/c1/1c0acdb3aa0856ddc4ecc55214578f896f2de16f400cf51627eb3c26c1c4/pydantic_core-2.46.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:de3a5c376f8cd94da9a1b8fd3dd1c16c7a7b216ed31dc8ce9fd7a22bf13b836e", size = 2360328, upload-time = "2026-04-20T14:41:43.991Z" }, + { url = "https://files.pythonhosted.org/packages/f0/d0/ef39cd0f4a926814f360e71c1adeab48ad214d9727e4deb48eedfb5bce1a/pydantic_core-2.46.3-cp311-cp311-win32.whl", hash = "sha256:fc331a5314ffddd5385b9ee9d0d2fee0b13c27e0e02dad71b1ae5d6561f51eeb", size = 1979464, upload-time = "2026-04-20T14:43:12.215Z" }, + { url = "https://files.pythonhosted.org/packages/18/9c/f41951b0d858e343f1cf09398b2a7b3014013799744f2c4a8ad6a3eec4f2/pydantic_core-2.46.3-cp311-cp311-win_amd64.whl", hash = "sha256:b5b9c6cf08a8a5e502698f5e153056d12c34b8fb30317e0c5fd06f45162a6346", size = 2070837, upload-time = "2026-04-20T14:41:47.707Z" }, + { url = "https://files.pythonhosted.org/packages/9f/1e/264a17cd582f6ed50950d4d03dd5fefd84e570e238afe1cb3e25cf238769/pydantic_core-2.46.3-cp311-cp311-win_arm64.whl", hash = "sha256:5dfd51cf457482f04ec49491811a2b8fd5b843b64b11eecd2d7a1ee596ea78a6", size = 2053647, upload-time = "2026-04-20T14:42:27.535Z" }, + { url = "https://files.pythonhosted.org/packages/4b/cb/5b47425556ecc1f3fe18ed2a0083188aa46e1dd812b06e406475b3a5d536/pydantic_core-2.46.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b11b59b3eee90a80a36701ddb4576d9ae31f93f05cb9e277ceaa09e6bf074a67", size = 2101946, upload-time = "2026-04-20T14:40:52.581Z" }, + { url = "https://files.pythonhosted.org/packages/a1/4f/2fb62c2267cae99b815bbf4a7b9283812c88ca3153ef29f7707200f1d4e5/pydantic_core-2.46.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:af8653713055ea18a3abc1537fe2ebc42f5b0bbb768d1eb79fd74eb47c0ac089", size = 1951612, upload-time = "2026-04-20T14:42:42.996Z" }, + { url = "https://files.pythonhosted.org/packages/50/6e/b7348fd30d6556d132cddd5bd79f37f96f2601fe0608afac4f5fb01ec0b3/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:75a519dab6d63c514f3a81053e5266c549679e4aa88f6ec57f2b7b854aceb1b0", size = 1977027, upload-time = "2026-04-20T14:42:02.001Z" }, + { url = "https://files.pythonhosted.org/packages/82/11/31d60ee2b45540d3fb0b29302a393dbc01cd771c473f5b5147bcd353e593/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a6cd87cb1575b1ad05ba98894c5b5c96411ef678fa2f6ed2576607095b8d9789", size = 2063008, upload-time = "2026-04-20T14:44:17.952Z" }, + { url = "https://files.pythonhosted.org/packages/8a/db/3a9d1957181b59258f44a2300ab0f0be9d1e12d662a4f57bb31250455c52/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f80a55484b8d843c8ada81ebf70a682f3f00a3d40e378c06cf17ecb44d280d7d", size = 2233082, upload-time = "2026-04-20T14:40:57.934Z" }, + { url = "https://files.pythonhosted.org/packages/9c/e1/3277c38792aeb5cfb18c2f0c5785a221d9ff4e149abbe1184d53d5f72273/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3861f1731b90c50a3266316b9044f5c9b405eecb8e299b0a7120596334e4fe9c", size = 2304615, upload-time = "2026-04-20T14:42:12.584Z" }, + { url = "https://files.pythonhosted.org/packages/5e/d5/e3d9717c9eba10855325650afd2a9cba8e607321697f18953af9d562da2f/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb528e295ed31570ac3dcc9bfdd6e0150bc11ce6168ac87a8082055cf1a67395", size = 2094380, upload-time = "2026-04-20T14:43:05.522Z" }, + { url = "https://files.pythonhosted.org/packages/a1/20/abac35dedcbfd66c6f0b03e4e3564511771d6c9b7ede10a362d03e110d9b/pydantic_core-2.46.3-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:367508faa4973b992b271ba1494acaab36eb7e8739d1e47be5035fb1ea225396", size = 2135429, upload-time = "2026-04-20T14:41:55.549Z" }, + { url = "https://files.pythonhosted.org/packages/6c/a5/41bfd1df69afad71b5cf0535055bccc73022715ad362edbc124bc1e021d7/pydantic_core-2.46.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5ad3c826fe523e4becf4fe39baa44286cff85ef137c729a2c5e269afbfd0905d", size = 2174582, upload-time = "2026-04-20T14:41:45.96Z" }, + { url = "https://files.pythonhosted.org/packages/79/65/38d86ea056b29b2b10734eb23329b7a7672ca604df4f2b6e9c02d4ee22fe/pydantic_core-2.46.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ec638c5d194ef8af27db69f16c954a09797c0dc25015ad6123eb2c73a4d271ca", size = 2187533, upload-time = "2026-04-20T14:40:55.367Z" }, + { url = "https://files.pythonhosted.org/packages/b6/55/a1129141678a2026badc539ad1dee0a71d06f54c2f06a4bd68c030ac781b/pydantic_core-2.46.3-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:28ed528c45446062ee66edb1d33df5d88828ae167de76e773a3c7f64bd14e976", size = 2332985, upload-time = "2026-04-20T14:44:13.05Z" }, + { url = "https://files.pythonhosted.org/packages/d7/60/cb26f4077719f709e54819f4e8e1d43f4091f94e285eb6bd21e1190a7b7c/pydantic_core-2.46.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aed19d0c783886d5bd86d80ae5030006b45e28464218747dcf83dabfdd092c7b", size = 2373670, upload-time = "2026-04-20T14:41:53.421Z" }, + { url = "https://files.pythonhosted.org/packages/6b/7e/c3f21882bdf1d8d086876f81b5e296206c69c6082551d776895de7801fa0/pydantic_core-2.46.3-cp312-cp312-win32.whl", hash = "sha256:06d5d8820cbbdb4147578c1fe7ffcd5b83f34508cb9f9ab76e807be7db6ff0a4", size = 1966722, upload-time = "2026-04-20T14:44:30.588Z" }, + { url = "https://files.pythonhosted.org/packages/57/be/6b5e757b859013ebfbd7adba02f23b428f37c86dcbf78b5bb0b4ffd36e99/pydantic_core-2.46.3-cp312-cp312-win_amd64.whl", hash = "sha256:c3212fda0ee959c1dd04c60b601ec31097aaa893573a3a1abd0a47bcac2968c1", size = 2072970, upload-time = "2026-04-20T14:42:54.248Z" }, + { url = "https://files.pythonhosted.org/packages/bf/f8/a989b21cc75e9a32d24192ef700eea606521221a89faa40c919ce884f2b1/pydantic_core-2.46.3-cp312-cp312-win_arm64.whl", hash = "sha256:f1f8338dd7a7f31761f1f1a3c47503a9a3b34eea3c8b01fa6ee96408affb5e72", size = 2035963, upload-time = "2026-04-20T14:44:20.4Z" }, + { url = "https://files.pythonhosted.org/packages/9b/3c/9b5e8eb9821936d065439c3b0fb1490ffa64163bfe7e1595985a47896073/pydantic_core-2.46.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:12bc98de041458b80c86c56b24df1d23832f3e166cbaff011f25d187f5c62c37", size = 2102109, upload-time = "2026-04-20T14:41:24.219Z" }, + { url = "https://files.pythonhosted.org/packages/91/97/1c41d1f5a19f241d8069f1e249853bcce378cdb76eec8ab636d7bc426280/pydantic_core-2.46.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:85348b8f89d2c3508b65b16c3c33a4da22b8215138d8b996912bb1532868885f", size = 1951820, upload-time = "2026-04-20T14:42:14.236Z" }, + { url = "https://files.pythonhosted.org/packages/30/b4/d03a7ae14571bc2b6b3c7b122441154720619afe9a336fa3a95434df5e2f/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1105677a6df914b1fb71a81b96c8cce7726857e1717d86001f29be06a25ee6f8", size = 1977785, upload-time = "2026-04-20T14:42:31.648Z" }, + { url = "https://files.pythonhosted.org/packages/ae/0c/4086f808834b59e3c8f1aa26df8f4b6d998cdcf354a143d18ef41529d1fe/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:87082cd65669a33adeba5470769e9704c7cf026cc30afb9cc77fd865578ebaad", size = 2062761, upload-time = "2026-04-20T14:40:37.093Z" }, + { url = "https://files.pythonhosted.org/packages/fa/71/a649be5a5064c2df0db06e0a512c2281134ed2fcc981f52a657936a7527c/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60e5f66e12c4f5212d08522963380eaaeac5ebd795826cfd19b2dfb0c7a52b9c", size = 2232989, upload-time = "2026-04-20T14:42:59.254Z" }, + { url = "https://files.pythonhosted.org/packages/a2/84/7756e75763e810b3a710f4724441d1ecc5883b94aacb07ca71c5fb5cfb69/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b6cdf19bf84128d5e7c37e8a73a0c5c10d51103a650ac585d42dd6ae233f2b7f", size = 2303975, upload-time = "2026-04-20T14:41:32.287Z" }, + { url = "https://files.pythonhosted.org/packages/6c/35/68a762e0c1e31f35fa0dac733cbd9f5b118042853698de9509c8e5bf128b/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:031bb17f4885a43773c8c763089499f242aee2ea85cf17154168775dccdecf35", size = 2095325, upload-time = "2026-04-20T14:42:47.685Z" }, + { url = "https://files.pythonhosted.org/packages/77/bf/1bf8c9a8e91836c926eae5e3e51dce009bf495a60ca56060689d3df3f340/pydantic_core-2.46.3-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:bcf2a8b2982a6673693eae7348ef3d8cf3979c1d63b54fca7c397a635cc68687", size = 2133368, upload-time = "2026-04-20T14:41:22.766Z" }, + { url = "https://files.pythonhosted.org/packages/e5/50/87d818d6bab915984995157ceb2380f5aac4e563dddbed6b56f0ed057aba/pydantic_core-2.46.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28e8cf2f52d72ced402a137145923a762cbb5081e48b34312f7a0c8f55928ec3", size = 2173908, upload-time = "2026-04-20T14:42:52.044Z" }, + { url = "https://files.pythonhosted.org/packages/91/88/a311fb306d0bd6185db41fa14ae888fb81d0baf648a761ae760d30819d33/pydantic_core-2.46.3-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:17eaface65d9fc5abb940003020309c1bf7a211f5f608d7870297c367e6f9022", size = 2186422, upload-time = "2026-04-20T14:43:29.55Z" }, + { url = "https://files.pythonhosted.org/packages/8f/79/28fd0d81508525ab2054fef7c77a638c8b5b0afcbbaeee493cf7c3fef7e1/pydantic_core-2.46.3-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:93fd339f23408a07e98950a89644f92c54d8729719a40b30c0a30bb9ebc55d23", size = 2332709, upload-time = "2026-04-20T14:42:16.134Z" }, + { url = "https://files.pythonhosted.org/packages/b3/21/795bf5fe5c0f379308b8ef19c50dedab2e7711dbc8d0c2acf08f1c7daa05/pydantic_core-2.46.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:23cbdb3aaa74dfe0837975dbf69b469753bbde8eacace524519ffdb6b6e89eb7", size = 2372428, upload-time = "2026-04-20T14:41:10.974Z" }, + { url = "https://files.pythonhosted.org/packages/45/b3/ed14c659cbe7605e3ef063077680a64680aec81eb1a04763a05190d49b7f/pydantic_core-2.46.3-cp313-cp313-win32.whl", hash = "sha256:610eda2e3838f401105e6326ca304f5da1e15393ae25dacae5c5c63f2c275b13", size = 1965601, upload-time = "2026-04-20T14:41:42.128Z" }, + { url = "https://files.pythonhosted.org/packages/ef/bb/adb70d9a762ddd002d723fbf1bd492244d37da41e3af7b74ad212609027e/pydantic_core-2.46.3-cp313-cp313-win_amd64.whl", hash = "sha256:68cc7866ed863db34351294187f9b729964c371ba33e31c26f478471c52e1ed0", size = 2071517, upload-time = "2026-04-20T14:43:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/52/eb/66faefabebfe68bd7788339c9c9127231e680b11906368c67ce112fdb47f/pydantic_core-2.46.3-cp313-cp313-win_arm64.whl", hash = "sha256:f64b5537ac62b231572879cd08ec05600308636a5d63bcbdb15063a466977bec", size = 2035802, upload-time = "2026-04-20T14:43:38.507Z" }, + { url = "https://files.pythonhosted.org/packages/7f/db/a7bcb4940183fda36022cd18ba8dd12f2dff40740ec7b58ce7457befa416/pydantic_core-2.46.3-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:afa3aa644f74e290cdede48a7b0bee37d1c35e71b05105f6b340d484af536d9b", size = 2097614, upload-time = "2026-04-20T14:44:38.374Z" }, + { url = "https://files.pythonhosted.org/packages/24/35/e4066358a22e3e99519db370494c7528f5a2aa1367370e80e27e20283543/pydantic_core-2.46.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ced3310e51aa425f7f77da8bbbb5212616655bedbe82c70944320bc1dbe5e018", size = 1951896, upload-time = "2026-04-20T14:40:53.996Z" }, + { url = "https://files.pythonhosted.org/packages/87/92/37cf4049d1636996e4b888c05a501f40a43ff218983a551d57f9d5e14f0d/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e29908922ce9da1a30b4da490bd1d3d82c01dcfdf864d2a74aacee674d0bfa34", size = 1979314, upload-time = "2026-04-20T14:41:49.446Z" }, + { url = "https://files.pythonhosted.org/packages/d8/36/9ff4d676dfbdfb2d591cf43f3d90ded01e15b1404fd101180ed2d62a2fd3/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0c9ff69140423eea8ed2d5477df3ba037f671f5e897d206d921bc9fdc39613e7", size = 2056133, upload-time = "2026-04-20T14:42:23.574Z" }, + { url = "https://files.pythonhosted.org/packages/bc/f0/405b442a4d7ba855b06eec8b2bf9c617d43b8432d099dfdc7bf999293495/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b675ab0a0d5b1c8fdb81195dc5bcefea3f3c240871cdd7ff9a2de8aa50772eb2", size = 2228726, upload-time = "2026-04-20T14:44:22.816Z" }, + { url = "https://files.pythonhosted.org/packages/e7/f8/65cd92dd5a0bd89ba277a98ecbfaf6fc36bbd3300973c7a4b826d6ab1391/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0087084960f209a9a4af50ecd1fb063d9ad3658c07bb81a7a53f452dacbfb2ba", size = 2301214, upload-time = "2026-04-20T14:44:48.792Z" }, + { url = "https://files.pythonhosted.org/packages/fd/86/ef96a4c6e79e7a2d0410826a68fbc0eccc0fd44aa733be199d5fcac3bb87/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed42e6cc8e1b0e2b9b96e2276bad70ae625d10d6d524aed0c93de974ae029f9f", size = 2099927, upload-time = "2026-04-20T14:41:40.196Z" }, + { url = "https://files.pythonhosted.org/packages/6d/53/269caf30e0096e0a8a8f929d1982a27b3879872cca2d917d17c2f9fdf4fe/pydantic_core-2.46.3-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:f1771ce258afb3e4201e67d154edbbae712a76a6081079fe247c2f53c6322c22", size = 2128789, upload-time = "2026-04-20T14:41:15.868Z" }, + { url = "https://files.pythonhosted.org/packages/00/b0/1a6d9b6a587e118482910c244a1c5acf4d192604174132efd12bf0ac486f/pydantic_core-2.46.3-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a7610b6a5242a6c736d8ad47fd5fff87fcfe8f833b281b1c409c3d6835d9227f", size = 2173815, upload-time = "2026-04-20T14:44:25.152Z" }, + { url = "https://files.pythonhosted.org/packages/87/56/e7e00d4041a7e62b5a40815590114db3b535bf3ca0bf4dca9f16cef25246/pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:ff5e7783bcc5476e1db448bf268f11cb257b1c276d3e89f00b5727be86dd0127", size = 2181608, upload-time = "2026-04-20T14:41:28.933Z" }, + { url = "https://files.pythonhosted.org/packages/e8/22/4bd23c3d41f7c185d60808a1de83c76cf5aeabf792f6c636a55c3b1ec7f9/pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:9d2e32edcc143bc01e95300671915d9ca052d4f745aa0a49c48d4803f8a85f2c", size = 2326968, upload-time = "2026-04-20T14:42:03.962Z" }, + { url = "https://files.pythonhosted.org/packages/24/ac/66cd45129e3915e5ade3b292cb3bc7fd537f58f8f8dbdaba6170f7cabb74/pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:6e42d83d1c6b87fa56b521479cff237e626a292f3b31b6345c15a99121b454c1", size = 2369842, upload-time = "2026-04-20T14:41:35.52Z" }, + { url = "https://files.pythonhosted.org/packages/a2/51/dd4248abb84113615473aa20d5545b7c4cd73c8644003b5259686f93996c/pydantic_core-2.46.3-cp314-cp314-win32.whl", hash = "sha256:07bc6d2a28c3adb4f7c6ae46aa4f2d2929af127f587ed44057af50bf1ce0f505", size = 1959661, upload-time = "2026-04-20T14:41:00.042Z" }, + { url = "https://files.pythonhosted.org/packages/20/eb/59980e5f1ae54a3b86372bd9f0fa373ea2d402e8cdcd3459334430f91e91/pydantic_core-2.46.3-cp314-cp314-win_amd64.whl", hash = "sha256:8940562319bc621da30714617e6a7eaa6b98c84e8c685bcdc02d7ed5e7c7c44e", size = 2071686, upload-time = "2026-04-20T14:43:16.471Z" }, + { url = "https://files.pythonhosted.org/packages/8c/db/1cf77e5247047dfee34bc01fa9bca134854f528c8eb053e144298893d370/pydantic_core-2.46.3-cp314-cp314-win_arm64.whl", hash = "sha256:5dcbbcf4d22210ced8f837c96db941bdb078f419543472aca5d9a0bb7cddc7df", size = 2026907, upload-time = "2026-04-20T14:43:31.732Z" }, + { url = "https://files.pythonhosted.org/packages/57/c0/b3df9f6a543276eadba0a48487b082ca1f201745329d97dbfa287034a230/pydantic_core-2.46.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:d0fe3dce1e836e418f912c1ad91c73357d03e556a4d286f441bf34fed2dbeecf", size = 2095047, upload-time = "2026-04-20T14:42:37.982Z" }, + { url = "https://files.pythonhosted.org/packages/66/57/886a938073b97556c168fd99e1a7305bb363cd30a6d2c76086bf0587b32a/pydantic_core-2.46.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9ce92e58abc722dac1bf835a6798a60b294e48eb0e625ec9fd994b932ac5feee", size = 1934329, upload-time = "2026-04-20T14:43:49.655Z" }, + { url = "https://files.pythonhosted.org/packages/0b/7c/b42eaa5c34b13b07ecb51da21761297a9b8eb43044c864a035999998f328/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a03e6467f0f5ab796a486146d1b887b2dc5e5f9b3288898c1b1c3ad974e53e4a", size = 1974847, upload-time = "2026-04-20T14:42:10.737Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9b/92b42db6543e7de4f99ae977101a2967b63122d4b6cf7773812da2d7d5b5/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2798b6ba041b9d70acfb9071a2ea13c8456dd1e6a5555798e41ba7b0790e329c", size = 2041742, upload-time = "2026-04-20T14:40:44.262Z" }, + { url = "https://files.pythonhosted.org/packages/0f/19/46fbe1efabb5aa2834b43b9454e70f9a83ad9c338c1291e48bdc4fecf167/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9be3e221bdc6d69abf294dcf7aff6af19c31a5cdcc8f0aa3b14be29df4bd03b1", size = 2236235, upload-time = "2026-04-20T14:41:27.307Z" }, + { url = "https://files.pythonhosted.org/packages/77/da/b3f95bc009ad60ec53120f5d16c6faa8cabdbe8a20d83849a1f2b8728148/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f13936129ce841f2a5ddf6f126fea3c43cd128807b5a59588c37cf10178c2e64", size = 2282633, upload-time = "2026-04-20T14:44:33.271Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6e/401336117722e28f32fb8220df676769d28ebdf08f2f4469646d404c43a3/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28b5f2ef03416facccb1c6ef744c69793175fd27e44ef15669201601cf423acb", size = 2109679, upload-time = "2026-04-20T14:44:41.065Z" }, + { url = "https://files.pythonhosted.org/packages/fc/53/b289f9bc8756a32fe718c46f55afaeaf8d489ee18d1a1e7be1db73f42cc4/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:830d1247d77ad23852314f069e9d7ddafeec5f684baf9d7e7065ed46a049c4e6", size = 2108342, upload-time = "2026-04-20T14:42:50.144Z" }, + { url = "https://files.pythonhosted.org/packages/10/5b/8292fc7c1f9111f1b2b7c1b0dcf1179edcd014fc3ea4517499f50b829d71/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0793c90c1a3c74966e7975eaef3ed30ebdff3260a0f815a62a22adc17e4c01c", size = 2157208, upload-time = "2026-04-20T14:42:08.133Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9e/f80044e9ec07580f057a89fc131f78dda7a58751ddf52bbe05eaf31db50f/pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:d2d0aead851b66f5245ec0c4fb2612ef457f8bbafefdf65a2bf9d6bac6140f47", size = 2167237, upload-time = "2026-04-20T14:42:25.412Z" }, + { url = "https://files.pythonhosted.org/packages/f8/84/6781a1b037f3b96be9227edbd1101f6d3946746056231bf4ac48cdff1a8d/pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:2f40e4246676beb31c5ce77c38a55ca4e465c6b38d11ea1bd935420568e0b1ab", size = 2312540, upload-time = "2026-04-20T14:40:40.313Z" }, + { url = "https://files.pythonhosted.org/packages/3e/db/19c0839feeb728e7df03255581f198dfdf1c2aeb1e174a8420b63c5252e5/pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:cf489cf8986c543939aeee17a09c04d6ffb43bfef8ca16fcbcc5cfdcbed24dba", size = 2369556, upload-time = "2026-04-20T14:41:09.427Z" }, + { url = "https://files.pythonhosted.org/packages/e0/15/3228774cb7cd45f5f721ddf1b2242747f4eb834d0c491f0c02d606f09fed/pydantic_core-2.46.3-cp314-cp314t-win32.whl", hash = "sha256:ffe0883b56cfc05798bf994164d2b2ff03efe2d22022a2bb080f3b626176dd56", size = 1949756, upload-time = "2026-04-20T14:41:25.717Z" }, + { url = "https://files.pythonhosted.org/packages/b8/2a/c79cf53fd91e5a87e30d481809f52f9a60dd221e39de66455cf04deaad37/pydantic_core-2.46.3-cp314-cp314t-win_amd64.whl", hash = "sha256:706d9d0ce9cf4593d07270d8e9f53b161f90c57d315aeec4fb4fd7a8b10240d8", size = 2051305, upload-time = "2026-04-20T14:43:18.627Z" }, + { url = "https://files.pythonhosted.org/packages/0b/db/d8182a7f1d9343a032265aae186eb063fe26ca4c40f256b21e8da4498e89/pydantic_core-2.46.3-cp314-cp314t-win_arm64.whl", hash = "sha256:77706aeb41df6a76568434701e0917da10692da28cb69d5fb6919ce5fdb07374", size = 2026310, upload-time = "2026-04-20T14:41:01.778Z" }, + { url = "https://files.pythonhosted.org/packages/66/7f/03dbad45cd3aa9083fbc93c210ae8b005af67e4136a14186950a747c6874/pydantic_core-2.46.3-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:9715525891ed524a0a1eb6d053c74d4d4ad5017677fb00af0b7c2644a31bae46", size = 2105683, upload-time = "2026-04-20T14:42:19.779Z" }, + { url = "https://files.pythonhosted.org/packages/26/22/4dc186ac8ea6b257e9855031f51b62a9637beac4d68ac06bee02f046f836/pydantic_core-2.46.3-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:9d2f400712a99a013aff420ef1eb9be077f8189a36c1e3ef87660b4e1088a874", size = 1940052, upload-time = "2026-04-20T14:43:59.274Z" }, + { url = "https://files.pythonhosted.org/packages/0d/ca/d376391a5aff1f2e8188960d7873543608130a870961c2b6b5236627c116/pydantic_core-2.46.3-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd2aab0e2e9dc2daf36bd2686c982535d5e7b1d930a1344a7bb6e82baab42a76", size = 1988172, upload-time = "2026-04-20T14:41:17.469Z" }, + { url = "https://files.pythonhosted.org/packages/0e/6b/523b9f85c23788755d6ab949329de692a2e3a584bc6beb67fef5e035aa9d/pydantic_core-2.46.3-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e9d76736da5f362fabfeea6a69b13b7f2be405c6d6966f06b2f6bfff7e64531", size = 2128596, upload-time = "2026-04-20T14:40:41.707Z" }, + { url = "https://files.pythonhosted.org/packages/34/42/f426db557e8ab2791bc7562052299944a118655496fbff99914e564c0a94/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:b12dd51f1187c2eb489af8e20f880362db98e954b54ab792fa5d92e8bcc6b803", size = 2091877, upload-time = "2026-04-20T14:43:27.091Z" }, + { url = "https://files.pythonhosted.org/packages/5c/4f/86a832a9d14df58e663bfdf4627dc00d3317c2bd583c4fb23390b0f04b8e/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:f00a0961b125f1a47af7bcc17f00782e12f4cd056f83416006b30111d941dfa3", size = 1932428, upload-time = "2026-04-20T14:40:45.781Z" }, + { url = "https://files.pythonhosted.org/packages/11/1a/fe857968954d93fb78e0d4b6df5c988c74c4aaa67181c60be7cfe327c0ca/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57697d7c056aca4bbb680200f96563e841a6386ac1129370a0102592f4dddff5", size = 1997550, upload-time = "2026-04-20T14:44:02.425Z" }, + { url = "https://files.pythonhosted.org/packages/17/eb/9d89ad2d9b0ba8cd65393d434471621b98912abb10fbe1df08e480ba57b5/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd35aa21299def8db7ef4fe5c4ff862941a9a158ca7b63d61e66fe67d30416b4", size = 2137657, upload-time = "2026-04-20T14:42:45.149Z" }, + { url = "https://files.pythonhosted.org/packages/1f/da/99d40830684f81dec901cac521b5b91c095394cc1084b9433393cde1c2df/pydantic_core-2.46.3-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:13afdd885f3d71280cf286b13b310ee0f7ccfefd1dbbb661514a474b726e2f25", size = 2107973, upload-time = "2026-04-20T14:42:06.175Z" }, + { url = "https://files.pythonhosted.org/packages/99/a5/87024121818d75bbb2a98ddbaf638e40e7a18b5e0f5492c9ca4b1b316107/pydantic_core-2.46.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f91c0aff3e3ee0928edd1232c57f643a7a003e6edf1860bc3afcdc749cb513f3", size = 1947191, upload-time = "2026-04-20T14:43:14.319Z" }, + { url = "https://files.pythonhosted.org/packages/60/62/0c1acfe10945b83a6a59d19fbaa92f48825381509e5701b855c08f13db76/pydantic_core-2.46.3-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6529d1d128321a58d30afcc97b49e98836542f68dd41b33c2e972bb9e5290536", size = 2123791, upload-time = "2026-04-20T14:43:22.766Z" }, + { url = "https://files.pythonhosted.org/packages/75/3e/3b2393b4c8f44285561dc30b00cf307a56a2eff7c483a824db3b8221ca51/pydantic_core-2.46.3-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:975c267cff4f7e7272eacbe50f6cc03ca9a3da4c4fbd66fffd89c94c1e311aa1", size = 2153197, upload-time = "2026-04-20T14:44:27.932Z" }, + { url = "https://files.pythonhosted.org/packages/ba/75/5af02fb35505051eee727c061f2881c555ab4f8ddb2d42da715a42c9731b/pydantic_core-2.46.3-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:2b8e4f2bbdf71415c544b4b1138b8060db7b6611bc927e8064c769f64bed651c", size = 2181073, upload-time = "2026-04-20T14:43:20.729Z" }, + { url = "https://files.pythonhosted.org/packages/10/92/7e0e1bd9ca3c68305db037560ca2876f89b2647deb2f8b6319005de37505/pydantic_core-2.46.3-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:e61ea8e9fff9606d09178f577ff8ccdd7206ff73d6552bcec18e1033c4254b85", size = 2315886, upload-time = "2026-04-20T14:44:04.826Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d8/101655f27eaf3e44558ead736b2795d12500598beed4683f279396fa186e/pydantic_core-2.46.3-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b504bda01bafc69b6d3c7a0c7f039dcf60f47fab70e06fe23f57b5c75bdc82b8", size = 2360528, upload-time = "2026-04-20T14:40:47.431Z" }, + { url = "https://files.pythonhosted.org/packages/07/0f/1c34a74c8d07136f0d729ffe5e1fdab04fbdaa7684f61a92f92511a84a15/pydantic_core-2.46.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:b00b76f7142fc60c762ce579bd29c8fa44aaa56592dd3c54fab3928d0d4ca6ff", size = 2184144, upload-time = "2026-04-20T14:42:57Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[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, upload-time = "2024-03-01T18:36:20.211Z" } +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, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "python-ulid" +version = "3.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/40/7e/0d6c82b5ccc71e7c833aed43d9e8468e1f2ff0be1b3f657a6fcafbb8433d/python_ulid-3.1.0.tar.gz", hash = "sha256:ff0410a598bc5f6b01b602851a3296ede6f91389f913a5d5f8c496003836f636", size = 93175, upload-time = "2025-08-18T16:09:26.305Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6c/a0/4ed6632b70a52de845df056654162acdebaf97c20e3212c559ac43e7216e/python_ulid-3.1.0-py3-none-any.whl", hash = "sha256:e2cdc979c8c877029b4b7a38a6fba3bc4578e4f109a308419ff4d3ccf0a46619", size = 11577, upload-time = "2025-08-18T16:09:25.047Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "pyzmq" +version = "27.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "implementation_name == 'pypy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/0b/3c9baedbdf613ecaa7aa07027780b8867f57b6293b6ee50de316c9f3222b/pyzmq-27.1.0.tar.gz", hash = "sha256:ac0765e3d44455adb6ddbf4417dcce460fc40a05978c08efdf2948072f6db540", size = 281750, upload-time = "2025-09-08T23:10:18.157Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/5d/305323ba86b284e6fcb0d842d6adaa2999035f70f8c38a9b6d21ad28c3d4/pyzmq-27.1.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:226b091818d461a3bef763805e75685e478ac17e9008f49fce2d3e52b3d58b86", size = 1333328, upload-time = "2025-09-08T23:07:45.946Z" }, + { url = "https://files.pythonhosted.org/packages/bd/a0/fc7e78a23748ad5443ac3275943457e8452da67fda347e05260261108cbc/pyzmq-27.1.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:0790a0161c281ca9723f804871b4027f2e8b5a528d357c8952d08cd1a9c15581", size = 908803, upload-time = "2025-09-08T23:07:47.551Z" }, + { url = "https://files.pythonhosted.org/packages/7e/22/37d15eb05f3bdfa4abea6f6d96eb3bb58585fbd3e4e0ded4e743bc650c97/pyzmq-27.1.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c895a6f35476b0c3a54e3eb6ccf41bf3018de937016e6e18748317f25d4e925f", size = 668836, upload-time = "2025-09-08T23:07:49.436Z" }, + { url = "https://files.pythonhosted.org/packages/b1/c4/2a6fe5111a01005fc7af3878259ce17684fabb8852815eda6225620f3c59/pyzmq-27.1.0-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bbf8d3630bf96550b3be8e1fc0fea5cbdc8d5466c1192887bd94869da17a63e", size = 857038, upload-time = "2025-09-08T23:07:51.234Z" }, + { url = "https://files.pythonhosted.org/packages/cb/eb/bfdcb41d0db9cd233d6fb22dc131583774135505ada800ebf14dfb0a7c40/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:15c8bd0fe0dabf808e2d7a681398c4e5ded70a551ab47482067a572c054c8e2e", size = 1657531, upload-time = "2025-09-08T23:07:52.795Z" }, + { url = "https://files.pythonhosted.org/packages/ab/21/e3180ca269ed4a0de5c34417dfe71a8ae80421198be83ee619a8a485b0c7/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:bafcb3dd171b4ae9f19ee6380dfc71ce0390fefaf26b504c0e5f628d7c8c54f2", size = 2034786, upload-time = "2025-09-08T23:07:55.047Z" }, + { url = "https://files.pythonhosted.org/packages/3b/b1/5e21d0b517434b7f33588ff76c177c5a167858cc38ef740608898cd329f2/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e829529fcaa09937189178115c49c504e69289abd39967cd8a4c215761373394", size = 1894220, upload-time = "2025-09-08T23:07:57.172Z" }, + { url = "https://files.pythonhosted.org/packages/03/f2/44913a6ff6941905efc24a1acf3d3cb6146b636c546c7406c38c49c403d4/pyzmq-27.1.0-cp311-cp311-win32.whl", hash = "sha256:6df079c47d5902af6db298ec92151db82ecb557af663098b92f2508c398bb54f", size = 567155, upload-time = "2025-09-08T23:07:59.05Z" }, + { url = "https://files.pythonhosted.org/packages/23/6d/d8d92a0eb270a925c9b4dd039c0b4dc10abc2fcbc48331788824ef113935/pyzmq-27.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:190cbf120fbc0fc4957b56866830def56628934a9d112aec0e2507aa6a032b97", size = 633428, upload-time = "2025-09-08T23:08:00.663Z" }, + { url = "https://files.pythonhosted.org/packages/ae/14/01afebc96c5abbbd713ecfc7469cfb1bc801c819a74ed5c9fad9a48801cb/pyzmq-27.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:eca6b47df11a132d1745eb3b5b5e557a7dae2c303277aa0e69c6ba91b8736e07", size = 559497, upload-time = "2025-09-08T23:08:02.15Z" }, + { url = "https://files.pythonhosted.org/packages/92/e7/038aab64a946d535901103da16b953c8c9cc9c961dadcbf3609ed6428d23/pyzmq-27.1.0-cp312-abi3-macosx_10_15_universal2.whl", hash = "sha256:452631b640340c928fa343801b0d07eb0c3789a5ffa843f6e1a9cee0ba4eb4fc", size = 1306279, upload-time = "2025-09-08T23:08:03.807Z" }, + { url = "https://files.pythonhosted.org/packages/e8/5e/c3c49fdd0f535ef45eefcc16934648e9e59dace4a37ee88fc53f6cd8e641/pyzmq-27.1.0-cp312-abi3-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1c179799b118e554b66da67d88ed66cd37a169f1f23b5d9f0a231b4e8d44a113", size = 895645, upload-time = "2025-09-08T23:08:05.301Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e5/b0b2504cb4e903a74dcf1ebae157f9e20ebb6ea76095f6cfffea28c42ecd/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3837439b7f99e60312f0c926a6ad437b067356dc2bc2ec96eb395fd0fe804233", size = 652574, upload-time = "2025-09-08T23:08:06.828Z" }, + { url = "https://files.pythonhosted.org/packages/f8/9b/c108cdb55560eaf253f0cbdb61b29971e9fb34d9c3499b0e96e4e60ed8a5/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43ad9a73e3da1fab5b0e7e13402f0b2fb934ae1c876c51d0afff0e7c052eca31", size = 840995, upload-time = "2025-09-08T23:08:08.396Z" }, + { url = "https://files.pythonhosted.org/packages/c2/bb/b79798ca177b9eb0825b4c9998c6af8cd2a7f15a6a1a4272c1d1a21d382f/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0de3028d69d4cdc475bfe47a6128eb38d8bc0e8f4d69646adfbcd840facbac28", size = 1642070, upload-time = "2025-09-08T23:08:09.989Z" }, + { url = "https://files.pythonhosted.org/packages/9c/80/2df2e7977c4ede24c79ae39dcef3899bfc5f34d1ca7a5b24f182c9b7a9ca/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_i686.whl", hash = "sha256:cf44a7763aea9298c0aa7dbf859f87ed7012de8bda0f3977b6fb1d96745df856", size = 2021121, upload-time = "2025-09-08T23:08:11.907Z" }, + { url = "https://files.pythonhosted.org/packages/46/bd/2d45ad24f5f5ae7e8d01525eb76786fa7557136555cac7d929880519e33a/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f30f395a9e6fbca195400ce833c731e7b64c3919aa481af4d88c3759e0cb7496", size = 1878550, upload-time = "2025-09-08T23:08:13.513Z" }, + { url = "https://files.pythonhosted.org/packages/e6/2f/104c0a3c778d7c2ab8190e9db4f62f0b6957b53c9d87db77c284b69f33ea/pyzmq-27.1.0-cp312-abi3-win32.whl", hash = "sha256:250e5436a4ba13885494412b3da5d518cd0d3a278a1ae640e113c073a5f88edd", size = 559184, upload-time = "2025-09-08T23:08:15.163Z" }, + { url = "https://files.pythonhosted.org/packages/fc/7f/a21b20d577e4100c6a41795842028235998a643b1ad406a6d4163ea8f53e/pyzmq-27.1.0-cp312-abi3-win_amd64.whl", hash = "sha256:9ce490cf1d2ca2ad84733aa1d69ce6855372cb5ce9223802450c9b2a7cba0ccf", size = 619480, upload-time = "2025-09-08T23:08:17.192Z" }, + { url = "https://files.pythonhosted.org/packages/78/c2/c012beae5f76b72f007a9e91ee9401cb88c51d0f83c6257a03e785c81cc2/pyzmq-27.1.0-cp312-abi3-win_arm64.whl", hash = "sha256:75a2f36223f0d535a0c919e23615fc85a1e23b71f40c7eb43d7b1dedb4d8f15f", size = 552993, upload-time = "2025-09-08T23:08:18.926Z" }, + { url = "https://files.pythonhosted.org/packages/60/cb/84a13459c51da6cec1b7b1dc1a47e6db6da50b77ad7fd9c145842750a011/pyzmq-27.1.0-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:93ad4b0855a664229559e45c8d23797ceac03183c7b6f5b4428152a6b06684a5", size = 1122436, upload-time = "2025-09-08T23:08:20.801Z" }, + { url = "https://files.pythonhosted.org/packages/dc/b6/94414759a69a26c3dd674570a81813c46a078767d931a6c70ad29fc585cb/pyzmq-27.1.0-cp313-cp313-android_24_x86_64.whl", hash = "sha256:fbb4f2400bfda24f12f009cba62ad5734148569ff4949b1b6ec3b519444342e6", size = 1156301, upload-time = "2025-09-08T23:08:22.47Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ad/15906493fd40c316377fd8a8f6b1f93104f97a752667763c9b9c1b71d42d/pyzmq-27.1.0-cp313-cp313t-macosx_10_15_universal2.whl", hash = "sha256:e343d067f7b151cfe4eb3bb796a7752c9d369eed007b91231e817071d2c2fec7", size = 1341197, upload-time = "2025-09-08T23:08:24.286Z" }, + { url = "https://files.pythonhosted.org/packages/14/1d/d343f3ce13db53a54cb8946594e567410b2125394dafcc0268d8dda027e0/pyzmq-27.1.0-cp313-cp313t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:08363b2011dec81c354d694bdecaef4770e0ae96b9afea70b3f47b973655cc05", size = 897275, upload-time = "2025-09-08T23:08:26.063Z" }, + { url = "https://files.pythonhosted.org/packages/69/2d/d83dd6d7ca929a2fc67d2c3005415cdf322af7751d773524809f9e585129/pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d54530c8c8b5b8ddb3318f481297441af102517602b569146185fa10b63f4fa9", size = 660469, upload-time = "2025-09-08T23:08:27.623Z" }, + { url = "https://files.pythonhosted.org/packages/3e/cd/9822a7af117f4bc0f1952dbe9ef8358eb50a24928efd5edf54210b850259/pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f3afa12c392f0a44a2414056d730eebc33ec0926aae92b5ad5cf26ebb6cc128", size = 847961, upload-time = "2025-09-08T23:08:29.672Z" }, + { url = "https://files.pythonhosted.org/packages/9a/12/f003e824a19ed73be15542f172fd0ec4ad0b60cf37436652c93b9df7c585/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c65047adafe573ff023b3187bb93faa583151627bc9c51fc4fb2c561ed689d39", size = 1650282, upload-time = "2025-09-08T23:08:31.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4a/e82d788ed58e9a23995cee70dbc20c9aded3d13a92d30d57ec2291f1e8a3/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:90e6e9441c946a8b0a667356f7078d96411391a3b8f80980315455574177ec97", size = 2024468, upload-time = "2025-09-08T23:08:33.543Z" }, + { url = "https://files.pythonhosted.org/packages/d9/94/2da0a60841f757481e402b34bf4c8bf57fa54a5466b965de791b1e6f747d/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:add071b2d25f84e8189aaf0882d39a285b42fa3853016ebab234a5e78c7a43db", size = 1885394, upload-time = "2025-09-08T23:08:35.51Z" }, + { url = "https://files.pythonhosted.org/packages/4f/6f/55c10e2e49ad52d080dc24e37adb215e5b0d64990b57598abc2e3f01725b/pyzmq-27.1.0-cp313-cp313t-win32.whl", hash = "sha256:7ccc0700cfdf7bd487bea8d850ec38f204478681ea02a582a8da8171b7f90a1c", size = 574964, upload-time = "2025-09-08T23:08:37.178Z" }, + { url = "https://files.pythonhosted.org/packages/87/4d/2534970ba63dd7c522d8ca80fb92777f362c0f321900667c615e2067cb29/pyzmq-27.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:8085a9fba668216b9b4323be338ee5437a235fe275b9d1610e422ccc279733e2", size = 641029, upload-time = "2025-09-08T23:08:40.595Z" }, + { url = "https://files.pythonhosted.org/packages/f6/fa/f8aea7a28b0641f31d40dea42d7ef003fded31e184ef47db696bc74cd610/pyzmq-27.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:6bb54ca21bcfe361e445256c15eedf083f153811c37be87e0514934d6913061e", size = 561541, upload-time = "2025-09-08T23:08:42.668Z" }, + { url = "https://files.pythonhosted.org/packages/87/45/19efbb3000956e82d0331bafca5d9ac19ea2857722fa2caacefb6042f39d/pyzmq-27.1.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ce980af330231615756acd5154f29813d553ea555485ae712c491cd483df6b7a", size = 1341197, upload-time = "2025-09-08T23:08:44.973Z" }, + { url = "https://files.pythonhosted.org/packages/48/43/d72ccdbf0d73d1343936296665826350cb1e825f92f2db9db3e61c2162a2/pyzmq-27.1.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1779be8c549e54a1c38f805e56d2a2e5c009d26de10921d7d51cfd1c8d4632ea", size = 897175, upload-time = "2025-09-08T23:08:46.601Z" }, + { url = "https://files.pythonhosted.org/packages/2f/2e/a483f73a10b65a9ef0161e817321d39a770b2acf8bcf3004a28d90d14a94/pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7200bb0f03345515df50d99d3db206a0a6bee1955fbb8c453c76f5bf0e08fb96", size = 660427, upload-time = "2025-09-08T23:08:48.187Z" }, + { url = "https://files.pythonhosted.org/packages/f5/d2/5f36552c2d3e5685abe60dfa56f91169f7a2d99bbaf67c5271022ab40863/pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01c0e07d558b06a60773744ea6251f769cd79a41a97d11b8bf4ab8f034b0424d", size = 847929, upload-time = "2025-09-08T23:08:49.76Z" }, + { url = "https://files.pythonhosted.org/packages/c4/2a/404b331f2b7bf3198e9945f75c4c521f0c6a3a23b51f7a4a401b94a13833/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:80d834abee71f65253c91540445d37c4c561e293ba6e741b992f20a105d69146", size = 1650193, upload-time = "2025-09-08T23:08:51.7Z" }, + { url = "https://files.pythonhosted.org/packages/1c/0b/f4107e33f62a5acf60e3ded67ed33d79b4ce18de432625ce2fc5093d6388/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:544b4e3b7198dde4a62b8ff6685e9802a9a1ebf47e77478a5eb88eca2a82f2fd", size = 2024388, upload-time = "2025-09-08T23:08:53.393Z" }, + { url = "https://files.pythonhosted.org/packages/0d/01/add31fe76512642fd6e40e3a3bd21f4b47e242c8ba33efb6809e37076d9b/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cedc4c68178e59a4046f97eca31b148ddcf51e88677de1ef4e78cf06c5376c9a", size = 1885316, upload-time = "2025-09-08T23:08:55.702Z" }, + { url = "https://files.pythonhosted.org/packages/c4/59/a5f38970f9bf07cee96128de79590bb354917914a9be11272cfc7ff26af0/pyzmq-27.1.0-cp314-cp314t-win32.whl", hash = "sha256:1f0b2a577fd770aa6f053211a55d1c47901f4d537389a034c690291485e5fe92", size = 587472, upload-time = "2025-09-08T23:08:58.18Z" }, + { url = "https://files.pythonhosted.org/packages/70/d8/78b1bad170f93fcf5e3536e70e8fadac55030002275c9a29e8f5719185de/pyzmq-27.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:19c9468ae0437f8074af379e986c5d3d7d7bfe033506af442e8c879732bedbe0", size = 661401, upload-time = "2025-09-08T23:08:59.802Z" }, + { url = "https://files.pythonhosted.org/packages/81/d6/4bfbb40c9a0b42fc53c7cf442f6385db70b40f74a783130c5d0a5aa62228/pyzmq-27.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dc5dbf68a7857b59473f7df42650c621d7e8923fb03fa74a526890f4d33cc4d7", size = 575170, upload-time = "2025-09-08T23:09:01.418Z" }, + { url = "https://files.pythonhosted.org/packages/4c/c6/c4dcdecdbaa70969ee1fdced6d7b8f60cfabe64d25361f27ac4665a70620/pyzmq-27.1.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:18770c8d3563715387139060d37859c02ce40718d1faf299abddcdcc6a649066", size = 836265, upload-time = "2025-09-08T23:09:49.376Z" }, + { url = "https://files.pythonhosted.org/packages/3e/79/f38c92eeaeb03a2ccc2ba9866f0439593bb08c5e3b714ac1d553e5c96e25/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:ac25465d42f92e990f8d8b0546b01c391ad431c3bf447683fdc40565941d0604", size = 800208, upload-time = "2025-09-08T23:09:51.073Z" }, + { url = "https://files.pythonhosted.org/packages/49/0e/3f0d0d335c6b3abb9b7b723776d0b21fa7f3a6c819a0db6097059aada160/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53b40f8ae006f2734ee7608d59ed661419f087521edbfc2149c3932e9c14808c", size = 567747, upload-time = "2025-09-08T23:09:52.698Z" }, + { url = "https://files.pythonhosted.org/packages/a1/cf/f2b3784d536250ffd4be70e049f3b60981235d70c6e8ce7e3ef21e1adb25/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f605d884e7c8be8fe1aa94e0a783bf3f591b84c24e4bc4f3e7564c82ac25e271", size = 747371, upload-time = "2025-09-08T23:09:54.563Z" }, + { url = "https://files.pythonhosted.org/packages/01/1b/5dbe84eefc86f48473947e2f41711aded97eecef1231f4558f1f02713c12/pyzmq-27.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c9f7f6e13dff2e44a6afeaf2cf54cee5929ad64afaf4d40b50f93c58fc687355", size = 544862, upload-time = "2025-09-08T23:09:56.509Z" }, +] + +[[package]] +name = "referencing" +version = "0.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, +] + +[[package]] +name = "rich" +version = "15.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, +] + +[[package]] +name = "rpds-py" +version = "0.30.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/6e/f964e88b3d2abee2a82c1ac8366da848fce1c6d834dc2132c3fda3970290/rpds_py-0.30.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425", size = 370157, upload-time = "2025-11-30T20:21:53.789Z" }, + { url = "https://files.pythonhosted.org/packages/94/ba/24e5ebb7c1c82e74c4e4f33b2112a5573ddc703915b13a073737b59b86e0/rpds_py-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d", size = 359676, upload-time = "2025-11-30T20:21:55.475Z" }, + { url = "https://files.pythonhosted.org/packages/84/86/04dbba1b087227747d64d80c3b74df946b986c57af0a9f0c98726d4d7a3b/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4", size = 389938, upload-time = "2025-11-30T20:21:57.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/bb/1463f0b1722b7f45431bdd468301991d1328b16cffe0b1c2918eba2c4eee/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f", size = 402932, upload-time = "2025-11-30T20:21:58.47Z" }, + { url = "https://files.pythonhosted.org/packages/99/ee/2520700a5c1f2d76631f948b0736cdf9b0acb25abd0ca8e889b5c62ac2e3/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4", size = 525830, upload-time = "2025-11-30T20:21:59.699Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ad/bd0331f740f5705cc555a5e17fdf334671262160270962e69a2bdef3bf76/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97", size = 412033, upload-time = "2025-11-30T20:22:00.991Z" }, + { url = "https://files.pythonhosted.org/packages/f8/1e/372195d326549bb51f0ba0f2ecb9874579906b97e08880e7a65c3bef1a99/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89", size = 390828, upload-time = "2025-11-30T20:22:02.723Z" }, + { url = "https://files.pythonhosted.org/packages/ab/2b/d88bb33294e3e0c76bc8f351a3721212713629ffca1700fa94979cb3eae8/rpds_py-0.30.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d", size = 404683, upload-time = "2025-11-30T20:22:04.367Z" }, + { url = "https://files.pythonhosted.org/packages/50/32/c759a8d42bcb5289c1fac697cd92f6fe01a018dd937e62ae77e0e7f15702/rpds_py-0.30.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038", size = 421583, upload-time = "2025-11-30T20:22:05.814Z" }, + { url = "https://files.pythonhosted.org/packages/2b/81/e729761dbd55ddf5d84ec4ff1f47857f4374b0f19bdabfcf929164da3e24/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7", size = 572496, upload-time = "2025-11-30T20:22:07.713Z" }, + { url = "https://files.pythonhosted.org/packages/14/f6/69066a924c3557c9c30baa6ec3a0aa07526305684c6f86c696b08860726c/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed", size = 598669, upload-time = "2025-11-30T20:22:09.312Z" }, + { url = "https://files.pythonhosted.org/packages/5f/48/905896b1eb8a05630d20333d1d8ffd162394127b74ce0b0784ae04498d32/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85", size = 561011, upload-time = "2025-11-30T20:22:11.309Z" }, + { url = "https://files.pythonhosted.org/packages/22/16/cd3027c7e279d22e5eb431dd3c0fbc677bed58797fe7581e148f3f68818b/rpds_py-0.30.0-cp311-cp311-win32.whl", hash = "sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c", size = 221406, upload-time = "2025-11-30T20:22:13.101Z" }, + { url = "https://files.pythonhosted.org/packages/fa/5b/e7b7aa136f28462b344e652ee010d4de26ee9fd16f1bfd5811f5153ccf89/rpds_py-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825", size = 236024, upload-time = "2025-11-30T20:22:14.853Z" }, + { url = "https://files.pythonhosted.org/packages/14/a6/364bba985e4c13658edb156640608f2c9e1d3ea3c81b27aa9d889fff0e31/rpds_py-0.30.0-cp311-cp311-win_arm64.whl", hash = "sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229", size = 229069, upload-time = "2025-11-30T20:22:16.577Z" }, + { url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086, upload-time = "2025-11-30T20:22:17.93Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053, upload-time = "2025-11-30T20:22:19.297Z" }, + { url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763, upload-time = "2025-11-30T20:22:21.661Z" }, + { url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951, upload-time = "2025-11-30T20:22:23.408Z" }, + { url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622, upload-time = "2025-11-30T20:22:25.16Z" }, + { url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492, upload-time = "2025-11-30T20:22:26.505Z" }, + { url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080, upload-time = "2025-11-30T20:22:27.934Z" }, + { url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680, upload-time = "2025-11-30T20:22:29.341Z" }, + { url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589, upload-time = "2025-11-30T20:22:31.469Z" }, + { url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289, upload-time = "2025-11-30T20:22:32.997Z" }, + { url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737, upload-time = "2025-11-30T20:22:34.419Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120, upload-time = "2025-11-30T20:22:35.903Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782, upload-time = "2025-11-30T20:22:37.271Z" }, + { url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463, upload-time = "2025-11-30T20:22:39.021Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868, upload-time = "2025-11-30T20:22:40.493Z" }, + { url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887, upload-time = "2025-11-30T20:22:41.812Z" }, + { url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904, upload-time = "2025-11-30T20:22:43.479Z" }, + { url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945, upload-time = "2025-11-30T20:22:44.819Z" }, + { url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783, upload-time = "2025-11-30T20:22:46.103Z" }, + { url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021, upload-time = "2025-11-30T20:22:47.458Z" }, + { url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589, upload-time = "2025-11-30T20:22:48.872Z" }, + { url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025, upload-time = "2025-11-30T20:22:50.196Z" }, + { url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895, upload-time = "2025-11-30T20:22:51.87Z" }, + { url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799, upload-time = "2025-11-30T20:22:53.341Z" }, + { url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731, upload-time = "2025-11-30T20:22:54.778Z" }, + { url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027, upload-time = "2025-11-30T20:22:56.212Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020, upload-time = "2025-11-30T20:22:58.2Z" }, + { url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139, upload-time = "2025-11-30T20:23:00.209Z" }, + { url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224, upload-time = "2025-11-30T20:23:02.008Z" }, + { url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645, upload-time = "2025-11-30T20:23:03.43Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443, upload-time = "2025-11-30T20:23:04.878Z" }, + { url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375, upload-time = "2025-11-30T20:23:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850, upload-time = "2025-11-30T20:23:07.825Z" }, + { url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812, upload-time = "2025-11-30T20:23:09.228Z" }, + { url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841, upload-time = "2025-11-30T20:23:11.186Z" }, + { url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149, upload-time = "2025-11-30T20:23:12.864Z" }, + { url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843, upload-time = "2025-11-30T20:23:14.638Z" }, + { url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507, upload-time = "2025-11-30T20:23:16.105Z" }, + { url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949, upload-time = "2025-11-30T20:23:17.539Z" }, + { url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790, upload-time = "2025-11-30T20:23:19.029Z" }, + { url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217, upload-time = "2025-11-30T20:23:20.885Z" }, + { url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" }, + { url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341, upload-time = "2025-11-30T20:23:24.449Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768, upload-time = "2025-11-30T20:23:25.908Z" }, + { url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099, upload-time = "2025-11-30T20:23:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192, upload-time = "2025-11-30T20:23:29.151Z" }, + { url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080, upload-time = "2025-11-30T20:23:30.785Z" }, + { url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841, upload-time = "2025-11-30T20:23:32.209Z" }, + { url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670, upload-time = "2025-11-30T20:23:33.742Z" }, + { url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005, upload-time = "2025-11-30T20:23:35.253Z" }, + { url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112, upload-time = "2025-11-30T20:23:36.842Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049, upload-time = "2025-11-30T20:23:38.343Z" }, + { url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661, upload-time = "2025-11-30T20:23:40.263Z" }, + { url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606, upload-time = "2025-11-30T20:23:42.186Z" }, + { url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126, upload-time = "2025-11-30T20:23:44.086Z" }, + { url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371, upload-time = "2025-11-30T20:23:46.004Z" }, + { url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298, upload-time = "2025-11-30T20:23:47.696Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604, upload-time = "2025-11-30T20:23:49.501Z" }, + { url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391, upload-time = "2025-11-30T20:23:50.96Z" }, + { url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868, upload-time = "2025-11-30T20:23:52.494Z" }, + { url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747, upload-time = "2025-11-30T20:23:54.036Z" }, + { url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795, upload-time = "2025-11-30T20:23:55.556Z" }, + { url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330, upload-time = "2025-11-30T20:23:57.033Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194, upload-time = "2025-11-30T20:23:58.637Z" }, + { url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340, upload-time = "2025-11-30T20:24:00.2Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765, upload-time = "2025-11-30T20:24:01.759Z" }, + { url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834, upload-time = "2025-11-30T20:24:03.687Z" }, + { url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470, upload-time = "2025-11-30T20:24:05.232Z" }, + { url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630, upload-time = "2025-11-30T20:24:06.878Z" }, + { url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148, upload-time = "2025-11-30T20:24:08.445Z" }, + { url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030, upload-time = "2025-11-30T20:24:10.956Z" }, + { url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570, upload-time = "2025-11-30T20:24:12.735Z" }, + { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" }, + { url = "https://files.pythonhosted.org/packages/69/71/3f34339ee70521864411f8b6992e7ab13ac30d8e4e3309e07c7361767d91/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58", size = 372292, upload-time = "2025-11-30T20:24:16.537Z" }, + { url = "https://files.pythonhosted.org/packages/57/09/f183df9b8f2d66720d2ef71075c59f7e1b336bec7ee4c48f0a2b06857653/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a", size = 362128, upload-time = "2025-11-30T20:24:18.086Z" }, + { url = "https://files.pythonhosted.org/packages/7a/68/5c2594e937253457342e078f0cc1ded3dd7b2ad59afdbf2d354869110a02/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb", size = 391542, upload-time = "2025-11-30T20:24:20.092Z" }, + { url = "https://files.pythonhosted.org/packages/49/5c/31ef1afd70b4b4fbdb2800249f34c57c64beb687495b10aec0365f53dfc4/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c", size = 404004, upload-time = "2025-11-30T20:24:22.231Z" }, + { url = "https://files.pythonhosted.org/packages/e3/63/0cfbea38d05756f3440ce6534d51a491d26176ac045e2707adc99bb6e60a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3", size = 527063, upload-time = "2025-11-30T20:24:24.302Z" }, + { url = "https://files.pythonhosted.org/packages/42/e6/01e1f72a2456678b0f618fc9a1a13f882061690893c192fcad9f2926553a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5", size = 413099, upload-time = "2025-11-30T20:24:25.916Z" }, + { url = "https://files.pythonhosted.org/packages/b8/25/8df56677f209003dcbb180765520c544525e3ef21ea72279c98b9aa7c7fb/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738", size = 392177, upload-time = "2025-11-30T20:24:27.834Z" }, + { url = "https://files.pythonhosted.org/packages/4a/b4/0a771378c5f16f8115f796d1f437950158679bcd2a7c68cf251cfb00ed5b/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f", size = 406015, upload-time = "2025-11-30T20:24:29.457Z" }, + { url = "https://files.pythonhosted.org/packages/36/d8/456dbba0af75049dc6f63ff295a2f92766b9d521fa00de67a2bd6427d57a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877", size = 423736, upload-time = "2025-11-30T20:24:31.22Z" }, + { url = "https://files.pythonhosted.org/packages/13/64/b4d76f227d5c45a7e0b796c674fd81b0a6c4fbd48dc29271857d8219571c/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a", size = 573981, upload-time = "2025-11-30T20:24:32.934Z" }, + { url = "https://files.pythonhosted.org/packages/20/91/092bacadeda3edf92bf743cc96a7be133e13a39cdbfd7b5082e7ab638406/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4", size = 599782, upload-time = "2025-11-30T20:24:35.169Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b7/b95708304cd49b7b6f82fdd039f1748b66ec2b21d6a45180910802f1abf1/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e", size = 562191, upload-time = "2025-11-30T20:24:36.853Z" }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + +[[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, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "soupsieve" +version = "2.8.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/ae/2d9c981590ed9999a0d91755b47fc74f74de286b0f5cee14c9269041e6c4/soupsieve-2.8.3.tar.gz", hash = "sha256:3267f1eeea4251fb42728b6dfb746edc9acaffc4a45b27e19450b676586e8349", size = 118627, upload-time = "2026-01-20T04:27:02.457Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/2c/1462b1d0a634697ae9e55b3cecdcb64788e8b7d63f54d923fcd0bb140aed/soupsieve-2.8.3-py3-none-any.whl", hash = "sha256:ed64f2ba4eebeab06cc4962affce381647455978ffc1e36bb79a545b91f45a95", size = 37016, upload-time = "2026-01-20T04:27:01.012Z" }, +] + +[[package]] +name = "tinycss2" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "webencodings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7a/fd/7a5ee21fd08ff70d3d33a5781c255cbe779659bd03278feb98b19ee550f4/tinycss2-1.4.0.tar.gz", hash = "sha256:10c0972f6fc0fbee87c3edb76549357415e94548c1ae10ebccdea16fb404a9b7", size = 87085, upload-time = "2024-10-24T14:58:29.895Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/34/ebdc18bae6aa14fbee1a08b63c015c72b64868ff7dae68808ab500c492e2/tinycss2-1.4.0-py3-none-any.whl", hash = "sha256:3a49cf47b7675da0b15d0c6e1df8df4ebd96e9394bb905a5775adb0d884c5289", size = 26610, upload-time = "2024-10-24T14:58:28.029Z" }, +] + +[[package]] +name = "tornado" +version = "6.5.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/f1/3173dfa4a18db4a9b03e5d55325559dab51ee653763bb8745a75af491286/tornado-6.5.5.tar.gz", hash = "sha256:192b8f3ea91bd7f1f50c06955416ed76c6b72f96779b962f07f911b91e8d30e9", size = 516006, upload-time = "2026-03-10T21:31:02.067Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/8c/77f5097695f4dd8255ecbd08b2a1ed8ba8b953d337804dd7080f199e12bf/tornado-6.5.5-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:487dc9cc380e29f58c7ab88f9e27cdeef04b2140862e5076a66fb6bb68bb1bfa", size = 445983, upload-time = "2026-03-10T21:30:44.28Z" }, + { url = "https://files.pythonhosted.org/packages/ab/5e/7625b76cd10f98f1516c36ce0346de62061156352353ef2da44e5c21523c/tornado-6.5.5-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:65a7f1d46d4bb41df1ac99f5fcb685fb25c7e61613742d5108b010975a9a6521", size = 444246, upload-time = "2026-03-10T21:30:46.571Z" }, + { url = "https://files.pythonhosted.org/packages/b2/04/7b5705d5b3c0fab088f434f9c83edac1573830ca49ccf29fb83bf7178eec/tornado-6.5.5-cp39-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e74c92e8e65086b338fd56333fb9a68b9f6f2fe7ad532645a290a464bcf46be5", size = 447229, upload-time = "2026-03-10T21:30:48.273Z" }, + { url = "https://files.pythonhosted.org/packages/34/01/74e034a30ef59afb4097ef8659515e96a39d910b712a89af76f5e4e1f93c/tornado-6.5.5-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:435319e9e340276428bbdb4e7fa732c2d399386d1de5686cb331ec8eee754f07", size = 448192, upload-time = "2026-03-10T21:30:51.22Z" }, + { url = "https://files.pythonhosted.org/packages/be/00/fe9e02c5a96429fce1a1d15a517f5d8444f9c412e0bb9eadfbe3b0fc55bf/tornado-6.5.5-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:3f54aa540bdbfee7b9eb268ead60e7d199de5021facd276819c193c0fb28ea4e", size = 448039, upload-time = "2026-03-10T21:30:53.52Z" }, + { url = "https://files.pythonhosted.org/packages/82/9e/656ee4cec0398b1d18d0f1eb6372c41c6b889722641d84948351ae19556d/tornado-6.5.5-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:36abed1754faeb80fbd6e64db2758091e1320f6bba74a4cf8c09cd18ccce8aca", size = 447445, upload-time = "2026-03-10T21:30:55.541Z" }, + { url = "https://files.pythonhosted.org/packages/5a/76/4921c00511f88af86a33de770d64141170f1cfd9c00311aea689949e274e/tornado-6.5.5-cp39-abi3-win32.whl", hash = "sha256:dd3eafaaeec1c7f2f8fdcd5f964e8907ad788fe8a5a32c4426fbbdda621223b7", size = 448582, upload-time = "2026-03-10T21:30:57.142Z" }, + { url = "https://files.pythonhosted.org/packages/2c/23/f6c6112a04d28eed765e374435fb1a9198f73e1ec4b4024184f21faeb1ad/tornado-6.5.5-cp39-abi3-win_amd64.whl", hash = "sha256:6443a794ba961a9f619b1ae926a2e900ac20c34483eea67be4ed8f1e58d3ef7b", size = 448990, upload-time = "2026-03-10T21:30:58.857Z" }, + { url = "https://files.pythonhosted.org/packages/b7/c8/876602cbc96469911f0939f703453c1157b0c826ecb05bdd32e023397d4e/tornado-6.5.5-cp39-abi3-win_arm64.whl", hash = "sha256:2c9a876e094109333f888539ddb2de4361743e5d21eece20688e3e351e4990a6", size = 448016, upload-time = "2026-03-10T21:31:00.43Z" }, +] + +[[package]] +name = "traitlets" +version = "5.14.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/eb/79/72064e6a701c2183016abbbfedaba506d81e30e232a68c9f0d6f6fcd1574/traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7", size = 161621, upload-time = "2024-04-19T11:11:49.746Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359, upload-time = "2024-04-19T11:11:46.763Z" }, +] + +[[package]] +name = "typer" +version = "0.24.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/b8/9ebb531b6c2d377af08ac6746a5df3425b21853a5d2260876919b58a2a4a/typer-0.24.2.tar.gz", hash = "sha256:ec070dcfca1408e85ee203c6365001e818c3b7fffe686fd07ff2d68095ca0480", size = 119849, upload-time = "2026-04-22T17:45:34.413Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/d1/9484b497e0a0410b901c12b8251c3e746e1e863f7d28419ffe06f7892fda/typer-0.24.2-py3-none-any.whl", hash = "sha256:b618bc3d721f9a8d30f3e05565be26416d06e9bcc29d49bc491dc26aba674fa8", size = 55977, upload-time = "2026-04-22T17:45:33.055Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "webencodings" +version = "0.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/02/ae6ceac1baeda530866a85075641cec12989bd8d31af6d5ab4a3e8c92f47/webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923", size = 9721, upload-time = "2017-04-05T20:21:34.189Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", size = 11774, upload-time = "2017-04-05T20:21:32.581Z" }, +] + +[[package]] +name = "websockets" +version = "16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", size = 179346, upload-time = "2026-01-10T09:23:47.181Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/db/de907251b4ff46ae804ad0409809504153b3f30984daf82a1d84a9875830/websockets-16.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:31a52addea25187bde0797a97d6fc3d2f92b6f72a9370792d65a6e84615ac8a8", size = 177340, upload-time = "2026-01-10T09:22:34.539Z" }, + { url = "https://files.pythonhosted.org/packages/f3/fa/abe89019d8d8815c8781e90d697dec52523fb8ebe308bf11664e8de1877e/websockets-16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:417b28978cdccab24f46400586d128366313e8a96312e4b9362a4af504f3bbad", size = 175022, upload-time = "2026-01-10T09:22:36.332Z" }, + { url = "https://files.pythonhosted.org/packages/58/5d/88ea17ed1ded2079358b40d31d48abe90a73c9e5819dbcde1606e991e2ad/websockets-16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:af80d74d4edfa3cb9ed973a0a5ba2b2a549371f8a741e0800cb07becdd20f23d", size = 175319, upload-time = "2026-01-10T09:22:37.602Z" }, + { url = "https://files.pythonhosted.org/packages/d2/ae/0ee92b33087a33632f37a635e11e1d99d429d3d323329675a6022312aac2/websockets-16.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:08d7af67b64d29823fed316505a89b86705f2b7981c07848fb5e3ea3020c1abe", size = 184631, upload-time = "2026-01-10T09:22:38.789Z" }, + { url = "https://files.pythonhosted.org/packages/c8/c5/27178df583b6c5b31b29f526ba2da5e2f864ecc79c99dae630a85d68c304/websockets-16.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7be95cfb0a4dae143eaed2bcba8ac23f4892d8971311f1b06f3c6b78952ee70b", size = 185870, upload-time = "2026-01-10T09:22:39.893Z" }, + { url = "https://files.pythonhosted.org/packages/87/05/536652aa84ddc1c018dbb7e2c4cbcd0db884580bf8e95aece7593fde526f/websockets-16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d6297ce39ce5c2e6feb13c1a996a2ded3b6832155fcfc920265c76f24c7cceb5", size = 185361, upload-time = "2026-01-10T09:22:41.016Z" }, + { url = "https://files.pythonhosted.org/packages/6d/e2/d5332c90da12b1e01f06fb1b85c50cfc489783076547415bf9f0a659ec19/websockets-16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1c1b30e4f497b0b354057f3467f56244c603a79c0d1dafce1d16c283c25f6e64", size = 184615, upload-time = "2026-01-10T09:22:42.442Z" }, + { url = "https://files.pythonhosted.org/packages/77/fb/d3f9576691cae9253b51555f841bc6600bf0a983a461c79500ace5a5b364/websockets-16.0-cp311-cp311-win32.whl", hash = "sha256:5f451484aeb5cafee1ccf789b1b66f535409d038c56966d6101740c1614b86c6", size = 178246, upload-time = "2026-01-10T09:22:43.654Z" }, + { url = "https://files.pythonhosted.org/packages/54/67/eaff76b3dbaf18dcddabc3b8c1dba50b483761cccff67793897945b37408/websockets-16.0-cp311-cp311-win_amd64.whl", hash = "sha256:8d7f0659570eefb578dacde98e24fb60af35350193e4f56e11190787bee77dac", size = 178684, upload-time = "2026-01-10T09:22:44.941Z" }, + { url = "https://files.pythonhosted.org/packages/84/7b/bac442e6b96c9d25092695578dda82403c77936104b5682307bd4deb1ad4/websockets-16.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:71c989cbf3254fbd5e84d3bff31e4da39c43f884e64f2551d14bb3c186230f00", size = 177365, upload-time = "2026-01-10T09:22:46.787Z" }, + { url = "https://files.pythonhosted.org/packages/b0/fe/136ccece61bd690d9c1f715baaeefd953bb2360134de73519d5df19d29ca/websockets-16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8b6e209ffee39ff1b6d0fa7bfef6de950c60dfb91b8fcead17da4ee539121a79", size = 175038, upload-time = "2026-01-10T09:22:47.999Z" }, + { url = "https://files.pythonhosted.org/packages/40/1e/9771421ac2286eaab95b8575b0cb701ae3663abf8b5e1f64f1fd90d0a673/websockets-16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86890e837d61574c92a97496d590968b23c2ef0aeb8a9bc9421d174cd378ae39", size = 175328, upload-time = "2026-01-10T09:22:49.809Z" }, + { url = "https://files.pythonhosted.org/packages/18/29/71729b4671f21e1eaa5d6573031ab810ad2936c8175f03f97f3ff164c802/websockets-16.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9b5aca38b67492ef518a8ab76851862488a478602229112c4b0d58d63a7a4d5c", size = 184915, upload-time = "2026-01-10T09:22:51.071Z" }, + { url = "https://files.pythonhosted.org/packages/97/bb/21c36b7dbbafc85d2d480cd65df02a1dc93bf76d97147605a8e27ff9409d/websockets-16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e0334872c0a37b606418ac52f6ab9cfd17317ac26365f7f65e203e2d0d0d359f", size = 186152, upload-time = "2026-01-10T09:22:52.224Z" }, + { url = "https://files.pythonhosted.org/packages/4a/34/9bf8df0c0cf88fa7bfe36678dc7b02970c9a7d5e065a3099292db87b1be2/websockets-16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a0b31e0b424cc6b5a04b8838bbaec1688834b2383256688cf47eb97412531da1", size = 185583, upload-time = "2026-01-10T09:22:53.443Z" }, + { url = "https://files.pythonhosted.org/packages/47/88/4dd516068e1a3d6ab3c7c183288404cd424a9a02d585efbac226cb61ff2d/websockets-16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:485c49116d0af10ac698623c513c1cc01c9446c058a4e61e3bf6c19dff7335a2", size = 184880, upload-time = "2026-01-10T09:22:55.033Z" }, + { url = "https://files.pythonhosted.org/packages/91/d6/7d4553ad4bf1c0421e1ebd4b18de5d9098383b5caa1d937b63df8d04b565/websockets-16.0-cp312-cp312-win32.whl", hash = "sha256:eaded469f5e5b7294e2bdca0ab06becb6756ea86894a47806456089298813c89", size = 178261, upload-time = "2026-01-10T09:22:56.251Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f0/f3a17365441ed1c27f850a80b2bc680a0fa9505d733fe152fdf5e98c1c0b/websockets-16.0-cp312-cp312-win_amd64.whl", hash = "sha256:5569417dc80977fc8c2d43a86f78e0a5a22fee17565d78621b6bb264a115d4ea", size = 178693, upload-time = "2026-01-10T09:22:57.478Z" }, + { url = "https://files.pythonhosted.org/packages/cc/9c/baa8456050d1c1b08dd0ec7346026668cbc6f145ab4e314d707bb845bf0d/websockets-16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:878b336ac47938b474c8f982ac2f7266a540adc3fa4ad74ae96fea9823a02cc9", size = 177364, upload-time = "2026-01-10T09:22:59.333Z" }, + { url = "https://files.pythonhosted.org/packages/7e/0c/8811fc53e9bcff68fe7de2bcbe75116a8d959ac699a3200f4847a8925210/websockets-16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:52a0fec0e6c8d9a784c2c78276a48a2bdf099e4ccc2a4cad53b27718dbfd0230", size = 175039, upload-time = "2026-01-10T09:23:01.171Z" }, + { url = "https://files.pythonhosted.org/packages/aa/82/39a5f910cb99ec0b59e482971238c845af9220d3ab9fa76dd9162cda9d62/websockets-16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6578ed5b6981005df1860a56e3617f14a6c307e6a71b4fff8c48fdc50f3ed2c", size = 175323, upload-time = "2026-01-10T09:23:02.341Z" }, + { url = "https://files.pythonhosted.org/packages/bd/28/0a25ee5342eb5d5f297d992a77e56892ecb65e7854c7898fb7d35e9b33bd/websockets-16.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95724e638f0f9c350bb1c2b0a7ad0e83d9cc0c9259f3ea94e40d7b02a2179ae5", size = 184975, upload-time = "2026-01-10T09:23:03.756Z" }, + { url = "https://files.pythonhosted.org/packages/f9/66/27ea52741752f5107c2e41fda05e8395a682a1e11c4e592a809a90c6a506/websockets-16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0204dc62a89dc9d50d682412c10b3542d748260d743500a85c13cd1ee4bde82", size = 186203, upload-time = "2026-01-10T09:23:05.01Z" }, + { url = "https://files.pythonhosted.org/packages/37/e5/8e32857371406a757816a2b471939d51c463509be73fa538216ea52b792a/websockets-16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52ac480f44d32970d66763115edea932f1c5b1312de36df06d6b219f6741eed8", size = 185653, upload-time = "2026-01-10T09:23:06.301Z" }, + { url = "https://files.pythonhosted.org/packages/9b/67/f926bac29882894669368dc73f4da900fcdf47955d0a0185d60103df5737/websockets-16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6e5a82b677f8f6f59e8dfc34ec06ca6b5b48bc4fcda346acd093694cc2c24d8f", size = 184920, upload-time = "2026-01-10T09:23:07.492Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a1/3d6ccdcd125b0a42a311bcd15a7f705d688f73b2a22d8cf1c0875d35d34a/websockets-16.0-cp313-cp313-win32.whl", hash = "sha256:abf050a199613f64c886ea10f38b47770a65154dc37181bfaff70c160f45315a", size = 178255, upload-time = "2026-01-10T09:23:09.245Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ae/90366304d7c2ce80f9b826096a9e9048b4bb760e44d3b873bb272cba696b/websockets-16.0-cp313-cp313-win_amd64.whl", hash = "sha256:3425ac5cf448801335d6fdc7ae1eb22072055417a96cc6b31b3861f455fbc156", size = 178689, upload-time = "2026-01-10T09:23:10.483Z" }, + { url = "https://files.pythonhosted.org/packages/f3/1d/e88022630271f5bd349ed82417136281931e558d628dd52c4d8621b4a0b2/websockets-16.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0", size = 177406, upload-time = "2026-01-10T09:23:12.178Z" }, + { url = "https://files.pythonhosted.org/packages/f2/78/e63be1bf0724eeb4616efb1ae1c9044f7c3953b7957799abb5915bffd38e/websockets-16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:daa3b6ff70a9241cf6c7fc9e949d41232d9d7d26fd3522b1ad2b4d62487e9904", size = 175085, upload-time = "2026-01-10T09:23:13.511Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f4/d3c9220d818ee955ae390cf319a7c7a467beceb24f05ee7aaaa2414345ba/websockets-16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4", size = 175328, upload-time = "2026-01-10T09:23:14.727Z" }, + { url = "https://files.pythonhosted.org/packages/63/bc/d3e208028de777087e6fb2b122051a6ff7bbcca0d6df9d9c2bf1dd869ae9/websockets-16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:781caf5e8eee67f663126490c2f96f40906594cb86b408a703630f95550a8c3e", size = 185044, upload-time = "2026-01-10T09:23:15.939Z" }, + { url = "https://files.pythonhosted.org/packages/ad/6e/9a0927ac24bd33a0a9af834d89e0abc7cfd8e13bed17a86407a66773cc0e/websockets-16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caab51a72c51973ca21fa8a18bd8165e1a0183f1ac7066a182ff27107b71e1a4", size = 186279, upload-time = "2026-01-10T09:23:17.148Z" }, + { url = "https://files.pythonhosted.org/packages/b9/ca/bf1c68440d7a868180e11be653c85959502efd3a709323230314fda6e0b3/websockets-16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1", size = 185711, upload-time = "2026-01-10T09:23:18.372Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f8/fdc34643a989561f217bb477cbc47a3a07212cbda91c0e4389c43c296ebf/websockets-16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5e18a238a2b2249c9a9235466b90e96ae4795672598a58772dd806edc7ac6d3", size = 184982, upload-time = "2026-01-10T09:23:19.652Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d1/574fa27e233764dbac9c52730d63fcf2823b16f0856b3329fc6268d6ae4f/websockets-16.0-cp314-cp314-win32.whl", hash = "sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8", size = 177915, upload-time = "2026-01-10T09:23:21.458Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f1/ae6b937bf3126b5134ce1f482365fde31a357c784ac51852978768b5eff4/websockets-16.0-cp314-cp314-win_amd64.whl", hash = "sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d", size = 178381, upload-time = "2026-01-10T09:23:22.715Z" }, + { url = "https://files.pythonhosted.org/packages/06/9b/f791d1db48403e1f0a27577a6beb37afae94254a8c6f08be4a23e4930bc0/websockets-16.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a35539cacc3febb22b8f4d4a99cc79b104226a756aa7400adc722e83b0d03244", size = 177737, upload-time = "2026-01-10T09:23:24.523Z" }, + { url = "https://files.pythonhosted.org/packages/bd/40/53ad02341fa33b3ce489023f635367a4ac98b73570102ad2cdd770dacc9a/websockets-16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b784ca5de850f4ce93ec85d3269d24d4c82f22b7212023c974c401d4980ebc5e", size = 175268, upload-time = "2026-01-10T09:23:25.781Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/6158d4e459b984f949dcbbb0c5d270154c7618e11c01029b9bbd1bb4c4f9/websockets-16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:569d01a4e7fba956c5ae4fc988f0d4e187900f5497ce46339c996dbf24f17641", size = 175486, upload-time = "2026-01-10T09:23:27.033Z" }, + { url = "https://files.pythonhosted.org/packages/e5/2d/7583b30208b639c8090206f95073646c2c9ffd66f44df967981a64f849ad/websockets-16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50f23cdd8343b984957e4077839841146f67a3d31ab0d00e6b824e74c5b2f6e8", size = 185331, upload-time = "2026-01-10T09:23:28.259Z" }, + { url = "https://files.pythonhosted.org/packages/45/b0/cce3784eb519b7b5ad680d14b9673a31ab8dcb7aad8b64d81709d2430aa8/websockets-16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:152284a83a00c59b759697b7f9e9cddf4e3c7861dd0d964b472b70f78f89e80e", size = 186501, upload-time = "2026-01-10T09:23:29.449Z" }, + { url = "https://files.pythonhosted.org/packages/19/60/b8ebe4c7e89fb5f6cdf080623c9d92789a53636950f7abacfc33fe2b3135/websockets-16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944", size = 186062, upload-time = "2026-01-10T09:23:31.368Z" }, + { url = "https://files.pythonhosted.org/packages/88/a8/a080593f89b0138b6cba1b28f8df5673b5506f72879322288b031337c0b8/websockets-16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206", size = 185356, upload-time = "2026-01-10T09:23:32.627Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b6/b9afed2afadddaf5ebb2afa801abf4b0868f42f8539bfe4b071b5266c9fe/websockets-16.0-cp314-cp314t-win32.whl", hash = "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6", size = 178085, upload-time = "2026-01-10T09:23:33.816Z" }, + { url = "https://files.pythonhosted.org/packages/9f/3e/28135a24e384493fa804216b79a6a6759a38cc4ff59118787b9fb693df93/websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd", size = 178531, upload-time = "2026-01-10T09:23:35.016Z" }, + { url = "https://files.pythonhosted.org/packages/72/07/c98a68571dcf256e74f1f816b8cc5eae6eb2d3d5cfa44d37f801619d9166/websockets-16.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:349f83cd6c9a415428ee1005cadb5c2c56f4389bc06a9af16103c3bc3dcc8b7d", size = 174947, upload-time = "2026-01-10T09:23:36.166Z" }, + { url = "https://files.pythonhosted.org/packages/7e/52/93e166a81e0305b33fe416338be92ae863563fe7bce446b0f687b9df5aea/websockets-16.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:4a1aba3340a8dca8db6eb5a7986157f52eb9e436b74813764241981ca4888f03", size = 175260, upload-time = "2026-01-10T09:23:37.409Z" }, + { url = "https://files.pythonhosted.org/packages/56/0c/2dbf513bafd24889d33de2ff0368190a0e69f37bcfa19009ef819fe4d507/websockets-16.0-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f4a32d1bd841d4bcbffdcb3d2ce50c09c3909fbead375ab28d0181af89fd04da", size = 176071, upload-time = "2026-01-10T09:23:39.158Z" }, + { url = "https://files.pythonhosted.org/packages/a5/8f/aea9c71cc92bf9b6cc0f7f70df8f0b420636b6c96ef4feee1e16f80f75dd/websockets-16.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0298d07ee155e2e9fda5be8a9042200dd2e3bb0b8a38482156576f863a9d457c", size = 176968, upload-time = "2026-01-10T09:23:41.031Z" }, + { url = "https://files.pythonhosted.org/packages/9a/3f/f70e03f40ffc9a30d817eef7da1be72ee4956ba8d7255c399a01b135902a/websockets-16.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:a653aea902e0324b52f1613332ddf50b00c06fdaf7e92624fbf8c77c78fa5767", size = 178735, upload-time = "2026-01-10T09:23:42.259Z" }, + { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" }, +] + +[[package]] +name = "xxhash" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/02/84/30869e01909fb37a6cc7e18688ee8bf1e42d57e7e0777636bd47524c43c7/xxhash-3.6.0.tar.gz", hash = "sha256:f0162a78b13a0d7617b2845b90c763339d1f1d82bb04a4b07f4ab535cc5e05d6", size = 85160, upload-time = "2025-10-02T14:37:08.097Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/d4/cc2f0400e9154df4b9964249da78ebd72f318e35ccc425e9f403c392f22a/xxhash-3.6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b47bbd8cf2d72797f3c2772eaaac0ded3d3af26481a26d7d7d41dc2d3c46b04a", size = 32844, upload-time = "2025-10-02T14:34:14.037Z" }, + { url = "https://files.pythonhosted.org/packages/5e/ec/1cc11cd13e26ea8bc3cb4af4eaadd8d46d5014aebb67be3f71fb0b68802a/xxhash-3.6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2b6821e94346f96db75abaa6e255706fb06ebd530899ed76d32cd99f20dc52fa", size = 30809, upload-time = "2025-10-02T14:34:15.484Z" }, + { url = "https://files.pythonhosted.org/packages/04/5f/19fe357ea348d98ca22f456f75a30ac0916b51c753e1f8b2e0e6fb884cce/xxhash-3.6.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d0a9751f71a1a65ce3584e9cae4467651c7e70c9d31017fa57574583a4540248", size = 194665, upload-time = "2025-10-02T14:34:16.541Z" }, + { url = "https://files.pythonhosted.org/packages/90/3b/d1f1a8f5442a5fd8beedae110c5af7604dc37349a8e16519c13c19a9a2de/xxhash-3.6.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b29ee68625ab37b04c0b40c3fafdf24d2f75ccd778333cfb698f65f6c463f62", size = 213550, upload-time = "2025-10-02T14:34:17.878Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ef/3a9b05eb527457d5db13a135a2ae1a26c80fecd624d20f3e8dcc4cb170f3/xxhash-3.6.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6812c25fe0d6c36a46ccb002f40f27ac903bf18af9f6dd8f9669cb4d176ab18f", size = 212384, upload-time = "2025-10-02T14:34:19.182Z" }, + { url = "https://files.pythonhosted.org/packages/0f/18/ccc194ee698c6c623acbf0f8c2969811a8a4b6185af5e824cd27b9e4fd3e/xxhash-3.6.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4ccbff013972390b51a18ef1255ef5ac125c92dc9143b2d1909f59abc765540e", size = 445749, upload-time = "2025-10-02T14:34:20.659Z" }, + { url = "https://files.pythonhosted.org/packages/a5/86/cf2c0321dc3940a7aa73076f4fd677a0fb3e405cb297ead7d864fd90847e/xxhash-3.6.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:297b7fbf86c82c550e12e8fb71968b3f033d27b874276ba3624ea868c11165a8", size = 193880, upload-time = "2025-10-02T14:34:22.431Z" }, + { url = "https://files.pythonhosted.org/packages/82/fb/96213c8560e6f948a1ecc9a7613f8032b19ee45f747f4fca4eb31bb6d6ed/xxhash-3.6.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dea26ae1eb293db089798d3973a5fc928a18fdd97cc8801226fae705b02b14b0", size = 210912, upload-time = "2025-10-02T14:34:23.937Z" }, + { url = "https://files.pythonhosted.org/packages/40/aa/4395e669b0606a096d6788f40dbdf2b819d6773aa290c19e6e83cbfc312f/xxhash-3.6.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7a0b169aafb98f4284f73635a8e93f0735f9cbde17bd5ec332480484241aaa77", size = 198654, upload-time = "2025-10-02T14:34:25.644Z" }, + { url = "https://files.pythonhosted.org/packages/67/74/b044fcd6b3d89e9b1b665924d85d3f400636c23590226feb1eb09e1176ce/xxhash-3.6.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:08d45aef063a4531b785cd72de4887766d01dc8f362a515693df349fdb825e0c", size = 210867, upload-time = "2025-10-02T14:34:27.203Z" }, + { url = "https://files.pythonhosted.org/packages/bc/fd/3ce73bf753b08cb19daee1eb14aa0d7fe331f8da9c02dd95316ddfe5275e/xxhash-3.6.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:929142361a48ee07f09121fe9e96a84950e8d4df3bb298ca5d88061969f34d7b", size = 414012, upload-time = "2025-10-02T14:34:28.409Z" }, + { url = "https://files.pythonhosted.org/packages/ba/b3/5a4241309217c5c876f156b10778f3ab3af7ba7e3259e6d5f5c7d0129eb2/xxhash-3.6.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:51312c768403d8540487dbbfb557454cfc55589bbde6424456951f7fcd4facb3", size = 191409, upload-time = "2025-10-02T14:34:29.696Z" }, + { url = "https://files.pythonhosted.org/packages/c0/01/99bfbc15fb9abb9a72b088c1d95219fc4782b7d01fc835bd5744d66dd0b8/xxhash-3.6.0-cp311-cp311-win32.whl", hash = "sha256:d1927a69feddc24c987b337ce81ac15c4720955b667fe9b588e02254b80446fd", size = 30574, upload-time = "2025-10-02T14:34:31.028Z" }, + { url = "https://files.pythonhosted.org/packages/65/79/9d24d7f53819fe301b231044ea362ce64e86c74f6e8c8e51320de248b3e5/xxhash-3.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:26734cdc2d4ffe449b41d186bbeac416f704a482ed835d375a5c0cb02bc63fef", size = 31481, upload-time = "2025-10-02T14:34:32.062Z" }, + { url = "https://files.pythonhosted.org/packages/30/4e/15cd0e3e8772071344eab2961ce83f6e485111fed8beb491a3f1ce100270/xxhash-3.6.0-cp311-cp311-win_arm64.whl", hash = "sha256:d72f67ef8bf36e05f5b6c65e8524f265bd61071471cd4cf1d36743ebeeeb06b7", size = 27861, upload-time = "2025-10-02T14:34:33.555Z" }, + { url = "https://files.pythonhosted.org/packages/9a/07/d9412f3d7d462347e4511181dea65e47e0d0e16e26fbee2ea86a2aefb657/xxhash-3.6.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:01362c4331775398e7bb34e3ab403bc9ee9f7c497bc7dee6272114055277dd3c", size = 32744, upload-time = "2025-10-02T14:34:34.622Z" }, + { url = "https://files.pythonhosted.org/packages/79/35/0429ee11d035fc33abe32dca1b2b69e8c18d236547b9a9b72c1929189b9a/xxhash-3.6.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b7b2df81a23f8cb99656378e72501b2cb41b1827c0f5a86f87d6b06b69f9f204", size = 30816, upload-time = "2025-10-02T14:34:36.043Z" }, + { url = "https://files.pythonhosted.org/packages/b7/f2/57eb99aa0f7d98624c0932c5b9a170e1806406cdbcdb510546634a1359e0/xxhash-3.6.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:dc94790144e66b14f67b10ac8ed75b39ca47536bf8800eb7c24b50271ea0c490", size = 194035, upload-time = "2025-10-02T14:34:37.354Z" }, + { url = "https://files.pythonhosted.org/packages/4c/ed/6224ba353690d73af7a3f1c7cdb1fc1b002e38f783cb991ae338e1eb3d79/xxhash-3.6.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:93f107c673bccf0d592cdba077dedaf52fe7f42dcd7676eba1f6d6f0c3efffd2", size = 212914, upload-time = "2025-10-02T14:34:38.6Z" }, + { url = "https://files.pythonhosted.org/packages/38/86/fb6b6130d8dd6b8942cc17ab4d90e223653a89aa32ad2776f8af7064ed13/xxhash-3.6.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2aa5ee3444c25b69813663c9f8067dcfaa2e126dc55e8dddf40f4d1c25d7effa", size = 212163, upload-time = "2025-10-02T14:34:39.872Z" }, + { url = "https://files.pythonhosted.org/packages/ee/dc/e84875682b0593e884ad73b2d40767b5790d417bde603cceb6878901d647/xxhash-3.6.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7f99123f0e1194fa59cc69ad46dbae2e07becec5df50a0509a808f90a0f03f0", size = 445411, upload-time = "2025-10-02T14:34:41.569Z" }, + { url = "https://files.pythonhosted.org/packages/11/4f/426f91b96701ec2f37bb2b8cec664eff4f658a11f3fa9d94f0a887ea6d2b/xxhash-3.6.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:49e03e6fe2cac4a1bc64952dd250cf0dbc5ef4ebb7b8d96bce82e2de163c82a2", size = 193883, upload-time = "2025-10-02T14:34:43.249Z" }, + { url = "https://files.pythonhosted.org/packages/53/5a/ddbb83eee8e28b778eacfc5a85c969673e4023cdeedcfcef61f36731610b/xxhash-3.6.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bd17fede52a17a4f9a7bc4472a5867cb0b160deeb431795c0e4abe158bc784e9", size = 210392, upload-time = "2025-10-02T14:34:45.042Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c2/ff69efd07c8c074ccdf0a4f36fcdd3d27363665bcdf4ba399abebe643465/xxhash-3.6.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:6fb5f5476bef678f69db04f2bd1efbed3030d2aba305b0fc1773645f187d6a4e", size = 197898, upload-time = "2025-10-02T14:34:46.302Z" }, + { url = "https://files.pythonhosted.org/packages/58/ca/faa05ac19b3b622c7c9317ac3e23954187516298a091eb02c976d0d3dd45/xxhash-3.6.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:843b52f6d88071f87eba1631b684fcb4b2068cd2180a0224122fe4ef011a9374", size = 210655, upload-time = "2025-10-02T14:34:47.571Z" }, + { url = "https://files.pythonhosted.org/packages/d4/7a/06aa7482345480cc0cb597f5c875b11a82c3953f534394f620b0be2f700c/xxhash-3.6.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7d14a6cfaf03b1b6f5f9790f76880601ccc7896aff7ab9cd8978a939c1eb7e0d", size = 414001, upload-time = "2025-10-02T14:34:49.273Z" }, + { url = "https://files.pythonhosted.org/packages/23/07/63ffb386cd47029aa2916b3d2f454e6cc5b9f5c5ada3790377d5430084e7/xxhash-3.6.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:418daf3db71e1413cfe211c2f9a528456936645c17f46b5204705581a45390ae", size = 191431, upload-time = "2025-10-02T14:34:50.798Z" }, + { url = "https://files.pythonhosted.org/packages/0f/93/14fde614cadb4ddf5e7cebf8918b7e8fac5ae7861c1875964f17e678205c/xxhash-3.6.0-cp312-cp312-win32.whl", hash = "sha256:50fc255f39428a27299c20e280d6193d8b63b8ef8028995323bf834a026b4fbb", size = 30617, upload-time = "2025-10-02T14:34:51.954Z" }, + { url = "https://files.pythonhosted.org/packages/13/5d/0d125536cbe7565a83d06e43783389ecae0c0f2ed037b48ede185de477c0/xxhash-3.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:c0f2ab8c715630565ab8991b536ecded9416d615538be8ecddce43ccf26cbc7c", size = 31534, upload-time = "2025-10-02T14:34:53.276Z" }, + { url = "https://files.pythonhosted.org/packages/54/85/6ec269b0952ec7e36ba019125982cf11d91256a778c7c3f98a4c5043d283/xxhash-3.6.0-cp312-cp312-win_arm64.whl", hash = "sha256:eae5c13f3bc455a3bbb68bdc513912dc7356de7e2280363ea235f71f54064829", size = 27876, upload-time = "2025-10-02T14:34:54.371Z" }, + { url = "https://files.pythonhosted.org/packages/33/76/35d05267ac82f53ae9b0e554da7c5e281ee61f3cad44c743f0fcd354f211/xxhash-3.6.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:599e64ba7f67472481ceb6ee80fa3bd828fd61ba59fb11475572cc5ee52b89ec", size = 32738, upload-time = "2025-10-02T14:34:55.839Z" }, + { url = "https://files.pythonhosted.org/packages/31/a8/3fbce1cd96534a95e35d5120637bf29b0d7f5d8fa2f6374e31b4156dd419/xxhash-3.6.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7d8b8aaa30fca4f16f0c84a5c8d7ddee0e25250ec2796c973775373257dde8f1", size = 30821, upload-time = "2025-10-02T14:34:57.219Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ea/d387530ca7ecfa183cb358027f1833297c6ac6098223fd14f9782cd0015c/xxhash-3.6.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d597acf8506d6e7101a4a44a5e428977a51c0fadbbfd3c39650cca9253f6e5a6", size = 194127, upload-time = "2025-10-02T14:34:59.21Z" }, + { url = "https://files.pythonhosted.org/packages/ba/0c/71435dcb99874b09a43b8d7c54071e600a7481e42b3e3ce1eb5226a5711a/xxhash-3.6.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:858dc935963a33bc33490128edc1c12b0c14d9c7ebaa4e387a7869ecc4f3e263", size = 212975, upload-time = "2025-10-02T14:35:00.816Z" }, + { url = "https://files.pythonhosted.org/packages/84/7a/c2b3d071e4bb4a90b7057228a99b10d51744878f4a8a6dd643c8bd897620/xxhash-3.6.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba284920194615cb8edf73bf52236ce2e1664ccd4a38fdb543506413529cc546", size = 212241, upload-time = "2025-10-02T14:35:02.207Z" }, + { url = "https://files.pythonhosted.org/packages/81/5f/640b6eac0128e215f177df99eadcd0f1b7c42c274ab6a394a05059694c5a/xxhash-3.6.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4b54219177f6c6674d5378bd862c6aedf64725f70dd29c472eaae154df1a2e89", size = 445471, upload-time = "2025-10-02T14:35:03.61Z" }, + { url = "https://files.pythonhosted.org/packages/5e/1e/3c3d3ef071b051cc3abbe3721ffb8365033a172613c04af2da89d5548a87/xxhash-3.6.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:42c36dd7dbad2f5238950c377fcbf6811b1cdb1c444fab447960030cea60504d", size = 193936, upload-time = "2025-10-02T14:35:05.013Z" }, + { url = "https://files.pythonhosted.org/packages/2c/bd/4a5f68381939219abfe1c22a9e3a5854a4f6f6f3c4983a87d255f21f2e5d/xxhash-3.6.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f22927652cba98c44639ffdc7aaf35828dccf679b10b31c4ad72a5b530a18eb7", size = 210440, upload-time = "2025-10-02T14:35:06.239Z" }, + { url = "https://files.pythonhosted.org/packages/eb/37/b80fe3d5cfb9faff01a02121a0f4d565eb7237e9e5fc66e73017e74dcd36/xxhash-3.6.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b45fad44d9c5c119e9c6fbf2e1c656a46dc68e280275007bbfd3d572b21426db", size = 197990, upload-time = "2025-10-02T14:35:07.735Z" }, + { url = "https://files.pythonhosted.org/packages/d7/fd/2c0a00c97b9e18f72e1f240ad4e8f8a90fd9d408289ba9c7c495ed7dc05c/xxhash-3.6.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:6f2580ffab1a8b68ef2b901cde7e55fa8da5e4be0977c68f78fc80f3c143de42", size = 210689, upload-time = "2025-10-02T14:35:09.438Z" }, + { url = "https://files.pythonhosted.org/packages/93/86/5dd8076a926b9a95db3206aba20d89a7fc14dd5aac16e5c4de4b56033140/xxhash-3.6.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:40c391dd3cd041ebc3ffe6f2c862f402e306eb571422e0aa918d8070ba31da11", size = 414068, upload-time = "2025-10-02T14:35:11.162Z" }, + { url = "https://files.pythonhosted.org/packages/af/3c/0bb129170ee8f3650f08e993baee550a09593462a5cddd8e44d0011102b1/xxhash-3.6.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f205badabde7aafd1a31e8ca2a3e5a763107a71c397c4481d6a804eb5063d8bd", size = 191495, upload-time = "2025-10-02T14:35:12.971Z" }, + { url = "https://files.pythonhosted.org/packages/e9/3a/6797e0114c21d1725e2577508e24006fd7ff1d8c0c502d3b52e45c1771d8/xxhash-3.6.0-cp313-cp313-win32.whl", hash = "sha256:2577b276e060b73b73a53042ea5bd5203d3e6347ce0d09f98500f418a9fcf799", size = 30620, upload-time = "2025-10-02T14:35:14.129Z" }, + { url = "https://files.pythonhosted.org/packages/86/15/9bc32671e9a38b413a76d24722a2bf8784a132c043063a8f5152d390b0f9/xxhash-3.6.0-cp313-cp313-win_amd64.whl", hash = "sha256:757320d45d2fbcce8f30c42a6b2f47862967aea7bf458b9625b4bbe7ee390392", size = 31542, upload-time = "2025-10-02T14:35:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/39/c5/cc01e4f6188656e56112d6a8e0dfe298a16934b8c47a247236549a3f7695/xxhash-3.6.0-cp313-cp313-win_arm64.whl", hash = "sha256:457b8f85dec5825eed7b69c11ae86834a018b8e3df5e77783c999663da2f96d6", size = 27880, upload-time = "2025-10-02T14:35:16.315Z" }, + { url = "https://files.pythonhosted.org/packages/f3/30/25e5321c8732759e930c555176d37e24ab84365482d257c3b16362235212/xxhash-3.6.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a42e633d75cdad6d625434e3468126c73f13f7584545a9cf34e883aa1710e702", size = 32956, upload-time = "2025-10-02T14:35:17.413Z" }, + { url = "https://files.pythonhosted.org/packages/9f/3c/0573299560d7d9f8ab1838f1efc021a280b5ae5ae2e849034ef3dee18810/xxhash-3.6.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:568a6d743219e717b07b4e03b0a828ce593833e498c3b64752e0f5df6bfe84db", size = 31072, upload-time = "2025-10-02T14:35:18.844Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1c/52d83a06e417cd9d4137722693424885cc9878249beb3a7c829e74bf7ce9/xxhash-3.6.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bec91b562d8012dae276af8025a55811b875baace6af510412a5e58e3121bc54", size = 196409, upload-time = "2025-10-02T14:35:20.31Z" }, + { url = "https://files.pythonhosted.org/packages/e3/8e/c6d158d12a79bbd0b878f8355432075fc82759e356ab5a111463422a239b/xxhash-3.6.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78e7f2f4c521c30ad5e786fdd6bae89d47a32672a80195467b5de0480aa97b1f", size = 215736, upload-time = "2025-10-02T14:35:21.616Z" }, + { url = "https://files.pythonhosted.org/packages/bc/68/c4c80614716345d55071a396cf03d06e34b5f4917a467faf43083c995155/xxhash-3.6.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3ed0df1b11a79856df5ffcab572cbd6b9627034c1c748c5566fa79df9048a7c5", size = 214833, upload-time = "2025-10-02T14:35:23.32Z" }, + { url = "https://files.pythonhosted.org/packages/7e/e9/ae27c8ffec8b953efa84c7c4a6c6802c263d587b9fc0d6e7cea64e08c3af/xxhash-3.6.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0e4edbfc7d420925b0dd5e792478ed393d6e75ff8fc219a6546fb446b6a417b1", size = 448348, upload-time = "2025-10-02T14:35:25.111Z" }, + { url = "https://files.pythonhosted.org/packages/d7/6b/33e21afb1b5b3f46b74b6bd1913639066af218d704cc0941404ca717fc57/xxhash-3.6.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fba27a198363a7ef87f8c0f6b171ec36b674fe9053742c58dd7e3201c1ab30ee", size = 196070, upload-time = "2025-10-02T14:35:26.586Z" }, + { url = "https://files.pythonhosted.org/packages/96/b6/fcabd337bc5fa624e7203aa0fa7d0c49eed22f72e93229431752bddc83d9/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:794fe9145fe60191c6532fa95063765529770edcdd67b3d537793e8004cabbfd", size = 212907, upload-time = "2025-10-02T14:35:28.087Z" }, + { url = "https://files.pythonhosted.org/packages/4b/d3/9ee6160e644d660fcf176c5825e61411c7f62648728f69c79ba237250143/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:6105ef7e62b5ac73a837778efc331a591d8442f8ef5c7e102376506cb4ae2729", size = 200839, upload-time = "2025-10-02T14:35:29.857Z" }, + { url = "https://files.pythonhosted.org/packages/0d/98/e8de5baa5109394baf5118f5e72ab21a86387c4f89b0e77ef3e2f6b0327b/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:f01375c0e55395b814a679b3eea205db7919ac2af213f4a6682e01220e5fe292", size = 213304, upload-time = "2025-10-02T14:35:31.222Z" }, + { url = "https://files.pythonhosted.org/packages/7b/1d/71056535dec5c3177eeb53e38e3d367dd1d16e024e63b1cee208d572a033/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:d706dca2d24d834a4661619dcacf51a75c16d65985718d6a7d73c1eeeb903ddf", size = 416930, upload-time = "2025-10-02T14:35:32.517Z" }, + { url = "https://files.pythonhosted.org/packages/dc/6c/5cbde9de2cd967c322e651c65c543700b19e7ae3e0aae8ece3469bf9683d/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5f059d9faeacd49c0215d66f4056e1326c80503f51a1532ca336a385edadd033", size = 193787, upload-time = "2025-10-02T14:35:33.827Z" }, + { url = "https://files.pythonhosted.org/packages/19/fa/0172e350361d61febcea941b0cc541d6e6c8d65d153e85f850a7b256ff8a/xxhash-3.6.0-cp313-cp313t-win32.whl", hash = "sha256:1244460adc3a9be84731d72b8e80625788e5815b68da3da8b83f78115a40a7ec", size = 30916, upload-time = "2025-10-02T14:35:35.107Z" }, + { url = "https://files.pythonhosted.org/packages/ad/e6/e8cf858a2b19d6d45820f072eff1bea413910592ff17157cabc5f1227a16/xxhash-3.6.0-cp313-cp313t-win_amd64.whl", hash = "sha256:b1e420ef35c503869c4064f4a2f2b08ad6431ab7b229a05cce39d74268bca6b8", size = 31799, upload-time = "2025-10-02T14:35:36.165Z" }, + { url = "https://files.pythonhosted.org/packages/56/15/064b197e855bfb7b343210e82490ae672f8bc7cdf3ddb02e92f64304ee8a/xxhash-3.6.0-cp313-cp313t-win_arm64.whl", hash = "sha256:ec44b73a4220623235f67a996c862049f375df3b1052d9899f40a6382c32d746", size = 28044, upload-time = "2025-10-02T14:35:37.195Z" }, + { url = "https://files.pythonhosted.org/packages/7e/5e/0138bc4484ea9b897864d59fce9be9086030825bc778b76cb5a33a906d37/xxhash-3.6.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a40a3d35b204b7cc7643cbcf8c9976d818cb47befcfac8bbefec8038ac363f3e", size = 32754, upload-time = "2025-10-02T14:35:38.245Z" }, + { url = "https://files.pythonhosted.org/packages/18/d7/5dac2eb2ec75fd771957a13e5dda560efb2176d5203f39502a5fc571f899/xxhash-3.6.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a54844be970d3fc22630b32d515e79a90d0a3ddb2644d8d7402e3c4c8da61405", size = 30846, upload-time = "2025-10-02T14:35:39.6Z" }, + { url = "https://files.pythonhosted.org/packages/fe/71/8bc5be2bb00deb5682e92e8da955ebe5fa982da13a69da5a40a4c8db12fb/xxhash-3.6.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:016e9190af8f0a4e3741343777710e3d5717427f175adfdc3e72508f59e2a7f3", size = 194343, upload-time = "2025-10-02T14:35:40.69Z" }, + { url = "https://files.pythonhosted.org/packages/e7/3b/52badfb2aecec2c377ddf1ae75f55db3ba2d321c5e164f14461c90837ef3/xxhash-3.6.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4f6f72232f849eb9d0141e2ebe2677ece15adfd0fa599bc058aad83c714bb2c6", size = 213074, upload-time = "2025-10-02T14:35:42.29Z" }, + { url = "https://files.pythonhosted.org/packages/a2/2b/ae46b4e9b92e537fa30d03dbc19cdae57ed407e9c26d163895e968e3de85/xxhash-3.6.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:63275a8aba7865e44b1813d2177e0f5ea7eadad3dd063a21f7cf9afdc7054063", size = 212388, upload-time = "2025-10-02T14:35:43.929Z" }, + { url = "https://files.pythonhosted.org/packages/f5/80/49f88d3afc724b4ac7fbd664c8452d6db51b49915be48c6982659e0e7942/xxhash-3.6.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cd01fa2aa00d8b017c97eb46b9a794fbdca53fc14f845f5a328c71254b0abb7", size = 445614, upload-time = "2025-10-02T14:35:45.216Z" }, + { url = "https://files.pythonhosted.org/packages/ed/ba/603ce3961e339413543d8cd44f21f2c80e2a7c5cfe692a7b1f2cccf58f3c/xxhash-3.6.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0226aa89035b62b6a86d3c68df4d7c1f47a342b8683da2b60cedcddb46c4d95b", size = 194024, upload-time = "2025-10-02T14:35:46.959Z" }, + { url = "https://files.pythonhosted.org/packages/78/d1/8e225ff7113bf81545cfdcd79eef124a7b7064a0bba53605ff39590b95c2/xxhash-3.6.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c6e193e9f56e4ca4923c61238cdaced324f0feac782544eb4c6d55ad5cc99ddd", size = 210541, upload-time = "2025-10-02T14:35:48.301Z" }, + { url = "https://files.pythonhosted.org/packages/6f/58/0f89d149f0bad89def1a8dd38feb50ccdeb643d9797ec84707091d4cb494/xxhash-3.6.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9176dcaddf4ca963d4deb93866d739a343c01c969231dbe21680e13a5d1a5bf0", size = 198305, upload-time = "2025-10-02T14:35:49.584Z" }, + { url = "https://files.pythonhosted.org/packages/11/38/5eab81580703c4df93feb5f32ff8fa7fe1e2c51c1f183ee4e48d4bb9d3d7/xxhash-3.6.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c1ce4009c97a752e682b897aa99aef84191077a9433eb237774689f14f8ec152", size = 210848, upload-time = "2025-10-02T14:35:50.877Z" }, + { url = "https://files.pythonhosted.org/packages/5e/6b/953dc4b05c3ce678abca756416e4c130d2382f877a9c30a20d08ee6a77c0/xxhash-3.6.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:8cb2f4f679b01513b7adbb9b1b2f0f9cdc31b70007eaf9d59d0878809f385b11", size = 414142, upload-time = "2025-10-02T14:35:52.15Z" }, + { url = "https://files.pythonhosted.org/packages/08/a9/238ec0d4e81a10eb5026d4a6972677cbc898ba6c8b9dbaec12ae001b1b35/xxhash-3.6.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:653a91d7c2ab54a92c19ccf43508b6a555440b9be1bc8be553376778be7f20b5", size = 191547, upload-time = "2025-10-02T14:35:53.547Z" }, + { url = "https://files.pythonhosted.org/packages/f1/ee/3cf8589e06c2164ac77c3bf0aa127012801128f1feebf2a079272da5737c/xxhash-3.6.0-cp314-cp314-win32.whl", hash = "sha256:a756fe893389483ee8c394d06b5ab765d96e68fbbfe6fde7aa17e11f5720559f", size = 31214, upload-time = "2025-10-02T14:35:54.746Z" }, + { url = "https://files.pythonhosted.org/packages/02/5d/a19552fbc6ad4cb54ff953c3908bbc095f4a921bc569433d791f755186f1/xxhash-3.6.0-cp314-cp314-win_amd64.whl", hash = "sha256:39be8e4e142550ef69629c9cd71b88c90e9a5db703fecbcf265546d9536ca4ad", size = 32290, upload-time = "2025-10-02T14:35:55.791Z" }, + { url = "https://files.pythonhosted.org/packages/b1/11/dafa0643bc30442c887b55baf8e73353a344ee89c1901b5a5c54a6c17d39/xxhash-3.6.0-cp314-cp314-win_arm64.whl", hash = "sha256:25915e6000338999236f1eb68a02a32c3275ac338628a7eaa5a269c401995679", size = 28795, upload-time = "2025-10-02T14:35:57.162Z" }, + { url = "https://files.pythonhosted.org/packages/2c/db/0e99732ed7f64182aef4a6fb145e1a295558deec2a746265dcdec12d191e/xxhash-3.6.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c5294f596a9017ca5a3e3f8884c00b91ab2ad2933cf288f4923c3fd4346cf3d4", size = 32955, upload-time = "2025-10-02T14:35:58.267Z" }, + { url = "https://files.pythonhosted.org/packages/55/f4/2a7c3c68e564a099becfa44bb3d398810cc0ff6749b0d3cb8ccb93f23c14/xxhash-3.6.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1cf9dcc4ab9cff01dfbba78544297a3a01dafd60f3bde4e2bfd016cf7e4ddc67", size = 31072, upload-time = "2025-10-02T14:35:59.382Z" }, + { url = "https://files.pythonhosted.org/packages/c6/d9/72a29cddc7250e8a5819dad5d466facb5dc4c802ce120645630149127e73/xxhash-3.6.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:01262da8798422d0685f7cef03b2bd3f4f46511b02830861df548d7def4402ad", size = 196579, upload-time = "2025-10-02T14:36:00.838Z" }, + { url = "https://files.pythonhosted.org/packages/63/93/b21590e1e381040e2ca305a884d89e1c345b347404f7780f07f2cdd47ef4/xxhash-3.6.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51a73fb7cb3a3ead9f7a8b583ffd9b8038e277cdb8cb87cf890e88b3456afa0b", size = 215854, upload-time = "2025-10-02T14:36:02.207Z" }, + { url = "https://files.pythonhosted.org/packages/ce/b8/edab8a7d4fa14e924b29be877d54155dcbd8b80be85ea00d2be3413a9ed4/xxhash-3.6.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b9c6df83594f7df8f7f708ce5ebeacfc69f72c9fbaaababf6cf4758eaada0c9b", size = 214965, upload-time = "2025-10-02T14:36:03.507Z" }, + { url = "https://files.pythonhosted.org/packages/27/67/dfa980ac7f0d509d54ea0d5a486d2bb4b80c3f1bb22b66e6a05d3efaf6c0/xxhash-3.6.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:627f0af069b0ea56f312fd5189001c24578868643203bca1abbc2c52d3a6f3ca", size = 448484, upload-time = "2025-10-02T14:36:04.828Z" }, + { url = "https://files.pythonhosted.org/packages/8c/63/8ffc2cc97e811c0ca5d00ab36604b3ea6f4254f20b7bc658ca825ce6c954/xxhash-3.6.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aa912c62f842dfd013c5f21a642c9c10cd9f4c4e943e0af83618b4a404d9091a", size = 196162, upload-time = "2025-10-02T14:36:06.182Z" }, + { url = "https://files.pythonhosted.org/packages/4b/77/07f0e7a3edd11a6097e990f6e5b815b6592459cb16dae990d967693e6ea9/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b465afd7909db30168ab62afe40b2fcf79eedc0b89a6c0ab3123515dc0df8b99", size = 213007, upload-time = "2025-10-02T14:36:07.733Z" }, + { url = "https://files.pythonhosted.org/packages/ae/d8/bc5fa0d152837117eb0bef6f83f956c509332ce133c91c63ce07ee7c4873/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:a881851cf38b0a70e7c4d3ce81fc7afd86fbc2a024f4cfb2a97cf49ce04b75d3", size = 200956, upload-time = "2025-10-02T14:36:09.106Z" }, + { url = "https://files.pythonhosted.org/packages/26/a5/d749334130de9411783873e9b98ecc46688dad5db64ca6e04b02acc8b473/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:9b3222c686a919a0f3253cfc12bb118b8b103506612253b5baeaac10d8027cf6", size = 213401, upload-time = "2025-10-02T14:36:10.585Z" }, + { url = "https://files.pythonhosted.org/packages/89/72/abed959c956a4bfc72b58c0384bb7940663c678127538634d896b1195c10/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:c5aa639bc113e9286137cec8fadc20e9cd732b2cc385c0b7fa673b84fc1f2a93", size = 417083, upload-time = "2025-10-02T14:36:12.276Z" }, + { url = "https://files.pythonhosted.org/packages/0c/b3/62fd2b586283b7d7d665fb98e266decadf31f058f1cf6c478741f68af0cb/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5c1343d49ac102799905e115aee590183c3921d475356cb24b4de29a4bc56518", size = 193913, upload-time = "2025-10-02T14:36:14.025Z" }, + { url = "https://files.pythonhosted.org/packages/9a/9a/c19c42c5b3f5a4aad748a6d5b4f23df3bed7ee5445accc65a0fb3ff03953/xxhash-3.6.0-cp314-cp314t-win32.whl", hash = "sha256:5851f033c3030dd95c086b4a36a2683c2ff4a799b23af60977188b057e467119", size = 31586, upload-time = "2025-10-02T14:36:15.603Z" }, + { url = "https://files.pythonhosted.org/packages/03/d6/4cc450345be9924fd5dc8c590ceda1db5b43a0a889587b0ae81a95511360/xxhash-3.6.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0444e7967dac37569052d2409b00a8860c2135cff05502df4da80267d384849f", size = 32526, upload-time = "2025-10-02T14:36:16.708Z" }, + { url = "https://files.pythonhosted.org/packages/0f/c9/7243eb3f9eaabd1a88a5a5acadf06df2d83b100c62684b7425c6a11bcaa8/xxhash-3.6.0-cp314-cp314t-win_arm64.whl", hash = "sha256:bb79b1e63f6fd84ec778a4b1916dfe0a7c3fdb986c06addd5db3a0d413819d95", size = 28898, upload-time = "2025-10-02T14:36:17.843Z" }, + { url = "https://files.pythonhosted.org/packages/93/1e/8aec23647a34a249f62e2398c42955acd9b4c6ed5cf08cbea94dc46f78d2/xxhash-3.6.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0f7b7e2ec26c1666ad5fc9dbfa426a6a3367ceaf79db5dd76264659d509d73b0", size = 30662, upload-time = "2025-10-02T14:37:01.743Z" }, + { url = "https://files.pythonhosted.org/packages/b8/0b/b14510b38ba91caf43006209db846a696ceea6a847a0c9ba0a5b1adc53d6/xxhash-3.6.0-pp311-pypy311_pp73-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5dc1e14d14fa0f5789ec29a7062004b5933964bb9b02aae6622b8f530dc40296", size = 41056, upload-time = "2025-10-02T14:37:02.879Z" }, + { url = "https://files.pythonhosted.org/packages/50/55/15a7b8a56590e66ccd374bbfa3f9ffc45b810886c8c3b614e3f90bd2367c/xxhash-3.6.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:881b47fc47e051b37d94d13e7455131054b56749b91b508b0907eb07900d1c13", size = 36251, upload-time = "2025-10-02T14:37:04.44Z" }, + { url = "https://files.pythonhosted.org/packages/62/b2/5ac99a041a29e58e95f907876b04f7067a0242cb85b5f39e726153981503/xxhash-3.6.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c6dc31591899f5e5666f04cc2e529e69b4072827085c1ef15294d91a004bc1bd", size = 32481, upload-time = "2025-10-02T14:37:05.869Z" }, + { url = "https://files.pythonhosted.org/packages/7b/d9/8d95e906764a386a3d3b596f3c68bb63687dfca806373509f51ce8eea81f/xxhash-3.6.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:15e0dac10eb9309508bfc41f7f9deaa7755c69e35af835db9cb10751adebc35d", size = 31565, upload-time = "2025-10-02T14:37:06.966Z" }, +] diff --git a/sample_projects/17_resources/main.netrun.json b/sample_projects/17_resources/main.netrun.json new file mode 100644 index 00000000..a82adeb1 --- /dev/null +++ b/sample_projects/17_resources/main.netrun.json @@ -0,0 +1,49 @@ +{ + "pools": { + "threads": {"spec": {"type": "thread", "num_workers": 3}} + }, + "resources": { + "gpu": 1 + }, + "output_queues": { + "results": { + "ports": [ + ["job_1", "out"], + ["job_2", "out"], + ["job_3", "out"] + ] + } + }, + "graph": { + "nodes": [ + { + "name": "job_1", + "factory": "netrun.node_factories.from_function", + "factory_args": {"func": "nodes.gpu_job_1"}, + "execution_config": { + "pools": ["threads"], + "resources": {"gpu": 1} + } + }, + { + "name": "job_2", + "factory": "netrun.node_factories.from_function", + "factory_args": {"func": "nodes.gpu_job_2"}, + "execution_config": { + "pools": ["threads"], + "resources": {"gpu": 1} + } + }, + { + "name": "job_3", + "factory": "netrun.node_factories.from_function", + "factory_args": {"func": "nodes.gpu_job_3"}, + "execution_config": { + "pools": ["threads"], + "resources": {"gpu": 1} + } + } + ], + "edges": [] + } +} diff --git a/sample_projects/17_resources/main.py b/sample_projects/17_resources/main.py new file mode 100644 index 00000000..71462543 --- /dev/null +++ b/sample_projects/17_resources/main.py @@ -0,0 +1,36 @@ +"""Run the resources sample. + +Three GPU jobs with a 1-slot `gpu` resource. They all sit on a 3-worker +thread pool, so they could parallelize — but the resource forces them to +run serially. Total wall-clock time is ~3 × 0.2s, not ~0.2s. +""" + +import asyncio +import time +from pathlib import Path + +from netrun.core import Net, NetConfig + + +async def main(): + config = NetConfig.from_file(Path(__file__).parent / "main.netrun.json") + async with Net(config) as net: + net.inject_data("job_1", "trigger", ["go"]) + net.inject_data("job_2", "trigger", ["go"]) + net.inject_data("job_3", "trigger", ["go"]) + + start = time.time() + await net.run_until_blocked() + elapsed = time.time() - start + + print(f"\nTotal wall-clock: {elapsed:.2f}s (serial — expected ~0.6s, not ~0.2s)") + print("Results:", net.flush_output_queue("results")) + + print("\nNode logs:") + for node in ("job_1", "job_2", "job_3"): + for ts, msg in net.logs.for_node(node): + print(f" {ts.strftime('%H:%M:%S.%f')[:-3]} | {msg}", end="") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/sample_projects/17_resources/nodes.py b/sample_projects/17_resources/nodes.py new file mode 100644 index 00000000..3e5ce669 --- /dev/null +++ b/sample_projects/17_resources/nodes.py @@ -0,0 +1,36 @@ +"""Resource semaphore demonstration. + +Three "GPU jobs" that could in principle run concurrently on a thread pool. +But the net declares only ONE `gpu` slot, and each job requires one. So +they end up running serially — the second job blocks until the first +finishes and releases the slot, and so on. + +Compare timestamps in the output: the start of each job should follow the +end of the previous one, not be concurrent. +""" + +import time + + +def gpu_job_1(trigger, ctx) -> str: + started = time.time() + ctx.print(f"gpu_job_1: started") + time.sleep(0.2) + ctx.print(f"gpu_job_1: finished after {time.time() - started:.2f}s") + return "job_1 done" + + +def gpu_job_2(trigger, ctx) -> str: + started = time.time() + ctx.print(f"gpu_job_2: started") + time.sleep(0.2) + ctx.print(f"gpu_job_2: finished after {time.time() - started:.2f}s") + return "job_2 done" + + +def gpu_job_3(trigger, ctx) -> str: + started = time.time() + ctx.print(f"gpu_job_3: started") + time.sleep(0.2) + ctx.print(f"gpu_job_3: finished after {time.time() - started:.2f}s") + return "job_3 done" diff --git a/sample_projects/17_resources/pyproject.toml b/sample_projects/17_resources/pyproject.toml new file mode 100644 index 00000000..80afaa4b --- /dev/null +++ b/sample_projects/17_resources/pyproject.toml @@ -0,0 +1,9 @@ +[project] +name = "resources-sample" +version = "0.1.0" +description = "Demonstrates `resources` as a named semaphore" +requires-python = ">=3.11" +dependencies = ["netrun"] + +[tool.uv.sources] +netrun = { path = "../../netrun", editable = true } diff --git a/sample_projects/17_resources/uv.lock b/sample_projects/17_resources/uv.lock new file mode 100644 index 00000000..a2e1338a --- /dev/null +++ b/sample_projects/17_resources/uv.lock @@ -0,0 +1,1235 @@ +version = 1 +revision = 3 +requires-python = ">=3.11" + +[options] +exclude-newer = "2026-04-24T11:32:52.712702Z" +exclude-newer-span = "P7D" + +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "attrs" +version = "26.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, +] + +[[package]] +name = "beartype" +version = "0.22.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/94/1009e248bbfbab11397abca7193bea6626806be9a327d399810d523a07cb/beartype-0.22.9.tar.gz", hash = "sha256:8f82b54aa723a2848a56008d18875f91c1db02c32ef6a62319a002e3e25a975f", size = 1608866, upload-time = "2025-12-13T06:50:30.72Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/cc/18245721fa7747065ab478316c7fea7c74777d07f37ae60db2e84f8172e8/beartype-0.22.9-py3-none-any.whl", hash = "sha256:d16c9bbc61ea14637596c5f6fbff2ee99cbe3573e46a716401734ef50c3060c2", size = 1333658, upload-time = "2025-12-13T06:50:28.266Z" }, +] + +[[package]] +name = "beautifulsoup4" +version = "4.14.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "soupsieve" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c3/b0/1c6a16426d389813b48d95e26898aff79abbde42ad353958ad95cc8c9b21/beautifulsoup4-4.14.3.tar.gz", hash = "sha256:6292b1c5186d356bba669ef9f7f051757099565ad9ada5dd630bd9de5fa7fb86", size = 627737, upload-time = "2025-11-30T15:08:26.084Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-py3-none-any.whl", hash = "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb", size = 107721, upload-time = "2025-11-30T15:08:24.087Z" }, +] + +[[package]] +name = "bleach" +version = "6.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "webencodings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/07/18/3c8523962314be6bf4c8989c79ad9531c825210dd13a8669f6b84336e8bd/bleach-6.3.0.tar.gz", hash = "sha256:6f3b91b1c0a02bb9a78b5a454c92506aa0fdf197e1d5e114d2e00c6f64306d22", size = 203533, upload-time = "2025-10-27T17:57:39.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cd/3a/577b549de0cc09d95f11087ee63c739bba856cd3952697eec4c4bb91350a/bleach-6.3.0-py3-none-any.whl", hash = "sha256:fe10ec77c93ddf3d13a73b035abaac7a9f5e436513864ccdad516693213c65d6", size = 164437, upload-time = "2025-10-27T17:57:37.538Z" }, +] + +[package.optional-dependencies] +css = [ + { name = "tinycss2" }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, + { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, + { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, + { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, + { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, + { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, + { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, + { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, + { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, +] + +[[package]] +name = "click" +version = "8.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/63/f9e1ea081ce35720d8b92acde70daaedace594dc93b693c869e0d5910718/click-8.3.3.tar.gz", hash = "sha256:398329ad4837b2ff7cbe1dd166a4c0f8900c3ca3a218de04466f38f6497f18a2", size = 328061, upload-time = "2026-04-22T15:11:27.506Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl", hash = "sha256:a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613", size = 110502, upload-time = "2026-04-22T15:11:25.044Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "defusedxml" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520, upload-time = "2021-03-08T10:59:26.269Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" }, +] + +[[package]] +name = "diskcache" +version = "5.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3f/21/1c1ffc1a039ddcc459db43cc108658f32c57d271d7289a2794e401d0fdb6/diskcache-5.6.3.tar.gz", hash = "sha256:2c3a3fa2743d8535d832ec61c2054a1641f41775aa7c556758a109941e33e4fc", size = 67916, upload-time = "2023-08-31T06:12:00.316Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/27/4570e78fc0bf5ea0ca45eb1de3818a23787af9b390c0b0a0033a1b8236f9/diskcache-5.6.3-py3-none-any.whl", hash = "sha256:5e31b2d5fbad117cc363ebaf6b689474db18a1f6438bc82358b024abd4c2ca19", size = 45550, upload-time = "2023-08-31T06:11:58.822Z" }, +] + +[[package]] +name = "fastjsonschema" +version = "2.21.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/b5/23b216d9d985a956623b6bd12d4086b60f0059b27799f23016af04a74ea1/fastjsonschema-2.21.2.tar.gz", hash = "sha256:b1eb43748041c880796cd077f1a07c3d94e93ae84bba5ed36800a33554ae05de", size = 374130, upload-time = "2025-08-14T18:49:36.666Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/a8/20d0723294217e47de6d9e2e40fd4a9d2f7c4b6ef974babd482a59743694/fastjsonschema-2.21.2-py3-none-any.whl", hash = "sha256:1c797122d0a86c5cace2e54bf4e819c36223b552017172f32c5c024a6b77e463", size = 24024, upload-time = "2025-08-14T18:49:34.776Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "jsonschema" +version = "4.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, +] + +[[package]] +name = "jupyter-client" +version = "8.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jupyter-core" }, + { name = "python-dateutil" }, + { name = "pyzmq" }, + { name = "tornado" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/05/e4/ba649102a3bc3fbca54e7239fb924fd434c766f855693d86de0b1f2bec81/jupyter_client-8.8.0.tar.gz", hash = "sha256:d556811419a4f2d96c869af34e854e3f059b7cc2d6d01a9cd9c85c267691be3e", size = 348020, upload-time = "2026-01-08T13:55:47.938Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/0b/ceb7694d864abc0a047649aec263878acb9f792e1fec3e676f22dc9015e3/jupyter_client-8.8.0-py3-none-any.whl", hash = "sha256:f93a5b99c5e23a507b773d3a1136bd6e16c67883ccdbd9a829b0bbdb98cd7d7a", size = 107371, upload-time = "2026-01-08T13:55:45.562Z" }, +] + +[[package]] +name = "jupyter-core" +version = "5.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "platformdirs" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/49/9d1284d0dc65e2c757b74c6687b6d319b02f822ad039e5c512df9194d9dd/jupyter_core-5.9.1.tar.gz", hash = "sha256:4d09aaff303b9566c3ce657f580bd089ff5c91f5f89cf7d8846c3cdf465b5508", size = 89814, upload-time = "2025-10-16T19:19:18.444Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl", hash = "sha256:ebf87fdc6073d142e114c72c9e29a9d7ca03fad818c5d300ce2adc1fb0743407", size = 29032, upload-time = "2025-10-16T19:19:16.783Z" }, +] + +[[package]] +name = "jupyterlab-pygments" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/90/51/9187be60d989df97f5f0aba133fa54e7300f17616e065d1ada7d7646b6d6/jupyterlab_pygments-0.3.0.tar.gz", hash = "sha256:721aca4d9029252b11cfa9d185e5b5af4d54772bb8072f9b7036f4170054d35d", size = 512900, upload-time = "2023-11-23T09:26:37.44Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/dd/ead9d8ea85bf202d90cc513b533f9c363121c7792674f78e0d8a854b63b4/jupyterlab_pygments-0.3.0-py3-none-any.whl", hash = "sha256:841a89020971da1d8693f1a99997aefc5dc424bb1b251fd6322462a1b8842780", size = 15884, upload-time = "2023-11-23T09:26:34.325Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, + { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, + { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, + { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, + { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, + { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "mistune" +version = "3.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/55/d01f0c4b45ade6536c51170b9043db8b2ec6ddf4a35c7ea3f5f559ac935b/mistune-3.2.0.tar.gz", hash = "sha256:708487c8a8cdd99c9d90eb3ed4c3ed961246ff78ac82f03418f5183ab70e398a", size = 95467, upload-time = "2025-12-23T11:36:34.994Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/f7/4a5e785ec9fbd65146a27b6b70b6cdc161a66f2024e4b04ac06a67f5578b/mistune-3.2.0-py3-none-any.whl", hash = "sha256:febdc629a3c78616b94393c6580551e0e34cc289987ec6c35ed3f4be42d0eee1", size = 53598, upload-time = "2025-12-23T11:36:33.211Z" }, +] + +[[package]] +name = "nbclient" +version = "0.10.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jupyter-client" }, + { name = "jupyter-core" }, + { name = "nbformat" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/56/91/1c1d5a4b9a9ebba2b4e32b8c852c2975c872aec1fe42ab5e516b2cecd193/nbclient-0.10.4.tar.gz", hash = "sha256:1e54091b16e6da39e297b0ece3e10f6f29f4ac4e8ee515d29f8a7099bd6553c9", size = 62554, upload-time = "2025-12-23T07:45:46.369Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/a0/5b0c2f11142ed1dddec842457d3f65eaf71a0080894eb6f018755b319c3a/nbclient-0.10.4-py3-none-any.whl", hash = "sha256:9162df5a7373d70d606527300a95a975a47c137776cd942e52d9c7e29ff83440", size = 25465, upload-time = "2025-12-23T07:45:44.51Z" }, +] + +[[package]] +name = "nbconvert" +version = "7.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beautifulsoup4" }, + { name = "bleach", extra = ["css"] }, + { name = "defusedxml" }, + { name = "jinja2" }, + { name = "jupyter-core" }, + { name = "jupyterlab-pygments" }, + { name = "markupsafe" }, + { name = "mistune" }, + { name = "nbclient" }, + { name = "nbformat" }, + { name = "packaging" }, + { name = "pandocfilters" }, + { name = "pygments" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/01/b1/708e53fe2e429c103c6e6e159106bcf0357ac41aa4c28772bd8402339051/nbconvert-7.17.1.tar.gz", hash = "sha256:34d0d0a7e73ce3cbab6c5aae8f4f468797280b01fd8bd2ca746da8569eddd7d2", size = 865311, upload-time = "2026-04-08T00:44:14.914Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/f8/bb0a9d5f46819c821dc1f004aa2cc29b1d91453297dbf5ff20470f00f193/nbconvert-7.17.1-py3-none-any.whl", hash = "sha256:aa85c087b435e7bf1ffd03319f658e285f2b89eccab33bc1ba7025495ab3e7c8", size = 261927, upload-time = "2026-04-08T00:44:12.845Z" }, +] + +[[package]] +name = "nbformat" +version = "5.10.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fastjsonschema" }, + { name = "jsonschema" }, + { name = "jupyter-core" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/fd/91545e604bc3dad7dca9ed03284086039b294c6b3d75c0d2fa45f9e9caf3/nbformat-5.10.4.tar.gz", hash = "sha256:322168b14f937a5d11362988ecac2a4952d3d8e3a2cbeb2319584631226d5b3a", size = 142749, upload-time = "2024-04-04T11:20:37.371Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/82/0340caa499416c78e5d8f5f05947ae4bc3cba53c9f038ab6e9ed964e22f1/nbformat-5.10.4-py3-none-any.whl", hash = "sha256:3b48d6c8fbca4b299bf3982ea7db1af21580e4fec269ad087b9e81588891200b", size = 78454, upload-time = "2024-04-04T11:20:34.895Z" }, +] + +[[package]] +name = "nblite" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jinja2" }, + { name = "nbconvert" }, + { name = "nbformat" }, + { name = "notebookx-py" }, + { name = "pydantic" }, + { name = "pyyaml" }, + { name = "rich" }, + { name = "typer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/80/a5/e7a91b57e1dc17d0e546e95500388b957320588a563c20c053eb593b3353/nblite-1.2.1.tar.gz", hash = "sha256:0f5b129d93069f0f7502ddcc1f8d960af78eefb9d097b1b85bad088ec1618cbb", size = 325768, upload-time = "2026-03-15T10:50:07.001Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/4d/471d79351bab69bf96f12d3b689d430342857138bb45fb82cb08a19125d1/nblite-1.2.1-py3-none-any.whl", hash = "sha256:04083f525229f8d8445c08dc192286491ec63e730806c84faf226b9d33b35ca0", size = 105264, upload-time = "2026-03-15T10:50:05.235Z" }, +] + +[[package]] +name = "netrun" +version = "0.5.0" +source = { editable = "../../netrun" } +dependencies = [ + { name = "beartype" }, + { name = "diskcache" }, + { name = "nblite" }, + { name = "netrun-sim" }, + { name = "websockets" }, + { name = "xxhash" }, +] + +[package.metadata] +requires-dist = [ + { name = "beartype", specifier = ">=0.22.9" }, + { name = "cloudpickle", marker = "extra == 'serialization'", specifier = ">=3.0" }, + { name = "dill", marker = "extra == 'serialization'", specifier = ">=0.3" }, + { name = "diskcache", specifier = ">=5.6.3" }, + { name = "nblite", specifier = ">=1.1.1" }, + { name = "netrun-sim", editable = "../../netrun-sim/python" }, + { name = "pyinfra", marker = "extra == 'deploy'", specifier = ">=3.0" }, + { name = "websockets", specifier = ">=16.0" }, + { name = "xxhash", specifier = ">=3.6.0" }, +] +provides-extras = ["deploy", "serialization"] + +[package.metadata.requires-dev] +dev = [ + { name = "dev", editable = "../../netrun/src/dev" }, + { name = "jupyterlab", specifier = ">=4.5.1" }, + { name = "mypy", specifier = ">=1.19.1" }, + { name = "nblite", specifier = ">=1.1.5" }, + { name = "netrun-cli", editable = "../../netrun-cli" }, + { name = "pytest", specifier = ">=9.0.2" }, + { name = "pytest-asyncio", specifier = ">=1.3.0" }, + { name = "ruff", specifier = ">=0.8.0" }, +] + +[[package]] +name = "netrun-sim" +version = "0.2.0" +source = { editable = "../../netrun-sim/python" } +dependencies = [ + { name = "python-ulid" }, +] + +[package.metadata] +requires-dist = [{ name = "python-ulid", specifier = ">=1.0" }] + +[package.metadata.requires-dev] +dev = [ + { name = "git-cliff", specifier = ">=2.11.0" }, + { name = "jupyterlab", specifier = ">=4.3.8" }, + { name = "maturin", specifier = ">=1.10.2" }, + { name = "nblite", specifier = ">=1.1.1" }, + { name = "python-ulid", specifier = ">=1.1.0" }, + { name = "ruff", specifier = ">=0.14.11" }, + { name = "twine", specifier = ">=6.1.0" }, + { name = "typing-extensions", specifier = ">=4.13.2" }, +] + +[[package]] +name = "notebookx-py" +version = "0.1.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/8f/a052edfbddf51d4ec485bf05ec0b526fc9d4d476cbdea01b60a040b4dd4c/notebookx_py-0.1.8.tar.gz", hash = "sha256:cee26716a9436b7ee0771ec840c49d268068d1e7688b9a4d3da79c763cd07a11", size = 40139, upload-time = "2026-01-22T19:31:59.907Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/24/f1d34cf2dca8ad9c008345d6900460991535e9b708979a39e6c6df8580a6/notebookx_py-0.1.8-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:e30a0677f752acaa47bbee12f00b6a3a9caf48e1b10637699dc956c99dbcf17e", size = 762017, upload-time = "2026-01-22T19:31:48.983Z" }, + { url = "https://files.pythonhosted.org/packages/54/78/a43c065515b8a26a36e1b1ce96f15b166e4c5beb4d026317adc6db86074f/notebookx_py-0.1.8-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:6adfd281c4c4c4a6dea7ae9e9c5ed4e04359a1425a78d9e33acdd3914c579752", size = 732635, upload-time = "2026-01-22T19:31:51.027Z" }, + { url = "https://files.pythonhosted.org/packages/89/ed/7141c4baf3ef1d0ee80bd18b0c6c5b1ee45d6320c86c5c850fd2ab1d8d35/notebookx_py-0.1.8-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ae180b4ef7939b4567ca97e60c23acac79e5c17d66ccbca51f1b4faa72ed35a", size = 782241, upload-time = "2026-01-22T19:31:52.379Z" }, + { url = "https://files.pythonhosted.org/packages/ba/f3/b97946c846113f86537e202679d3322e06f5014d86e06fe061767d1cafd2/notebookx_py-0.1.8-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d34febdababaec0b5fca2355d3dbd6f69c3b0b596b26daec5ffa66c754bda94", size = 806947, upload-time = "2026-01-22T19:31:54.329Z" }, + { url = "https://files.pythonhosted.org/packages/07/0d/266611c951bea05711a2bf4791b035720a9daec1181d46589acc57d9f38c/notebookx_py-0.1.8-cp38-abi3-win_amd64.whl", hash = "sha256:fa4d013e4272d3ae714d5ca2206db9d263f85064ecb7380ee7bf722dee5d4625", size = 624603, upload-time = "2026-01-22T19:31:55.687Z" }, +] + +[[package]] +name = "packaging" +version = "26.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/df/de/0d2b39fb4af88a0258f3bac87dfcbb48e73fbdea4a2ed0e2213f9a4c2f9a/packaging-26.1.tar.gz", hash = "sha256:f042152b681c4bfac5cae2742a55e103d27ab2ec0f3d88037136b6bfe7c9c5de", size = 215519, upload-time = "2026-04-14T21:12:49.362Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/c2/920ef838e2f0028c8262f16101ec09ebd5969864e5a64c4c05fad0617c56/packaging-26.1-py3-none-any.whl", hash = "sha256:5d9c0669c6285e491e0ced2eee587eaf67b670d94a19e94e3984a481aba6802f", size = 95831, upload-time = "2026-04-14T21:12:47.56Z" }, +] + +[[package]] +name = "pandocfilters" +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/70/6f/3dd4940bbe001c06a65f88e36bad298bc7a0de5036115639926b0c5c0458/pandocfilters-1.5.1.tar.gz", hash = "sha256:002b4a555ee4ebc03f8b66307e287fa492e4a77b4ea14d3f934328297bb4939e", size = 8454, upload-time = "2024-01-18T20:08:13.726Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/af/4fbc8cab944db5d21b7e2a5b8e9211a03a79852b1157e2c102fcc61ac440/pandocfilters-1.5.1-py2.py3-none-any.whl", hash = "sha256:93be382804a9cdb0a7267585f157e5d1731bbe5545a85b268d6f5fe6232de2bc", size = 8663, upload-time = "2024-01-18T20:08:11.28Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.9.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9f/4a/0883b8e3802965322523f0b200ecf33d31f10991d0401162f4b23c698b42/platformdirs-4.9.6.tar.gz", hash = "sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a", size = 29400, upload-time = "2026-04-09T00:04:10.812Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/a6/a0a304dc33b49145b21f4808d763822111e67d1c3a32b524a1baf947b6e1/platformdirs-4.9.6-py3-none-any.whl", hash = "sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917", size = 21348, upload-time = "2026-04-09T00:04:09.463Z" }, +] + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + +[[package]] +name = "pydantic" +version = "2.13.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d9/e4/40d09941a2cebcb20609b86a559817d5b9291c49dd6f8c87e5feffbe703a/pydantic-2.13.3.tar.gz", hash = "sha256:af09e9d1d09f4e7fe37145c1f577e1d61ceb9a41924bf0094a36506285d0a84d", size = 844068, upload-time = "2026-04-20T14:46:43.632Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/0a/fd7d723f8f8153418fb40cf9c940e82004fce7e987026b08a68a36dd3fe7/pydantic-2.13.3-py3-none-any.whl", hash = "sha256:6db14ac8dfc9a1e57f87ea2c0de670c251240f43cb0c30a5130e9720dc612927", size = 471981, upload-time = "2026-04-20T14:46:41.402Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.46.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2a/ef/f7abb56c49382a246fd2ce9c799691e3c3e7175ec74b14d99e798bcddb1a/pydantic_core-2.46.3.tar.gz", hash = "sha256:41c178f65b8c29807239d47e6050262eb6bf84eb695e41101e62e38df4a5bc2c", size = 471412, upload-time = "2026-04-20T14:40:56.672Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/a2/1ba90a83e85a3f94c796b184f3efde9c72f2830dcda493eea8d59ba78e6d/pydantic_core-2.46.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ab124d49d0459b2373ecf54118a45c28a1e6d4192a533fbc915e70f556feb8e5", size = 2106740, upload-time = "2026-04-20T14:41:20.932Z" }, + { url = "https://files.pythonhosted.org/packages/b6/f6/99ae893c89a0b9d3daec9f95487aa676709aa83f67643b3f0abaf4ab628a/pydantic_core-2.46.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cca67d52a5c7a16aed2b3999e719c4bcf644074eac304a5d3d62dd70ae7d4b2c", size = 1948293, upload-time = "2026-04-20T14:43:42.115Z" }, + { url = "https://files.pythonhosted.org/packages/3e/b8/2e8e636dc9e3f16c2e16bf0849e24be82c5ee82c603c65fc0326666328fc/pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c024e08c0ba23e6fd68c771a521e9d6a792f2ebb0fa734296b36394dc30390e", size = 1973222, upload-time = "2026-04-20T14:41:57.841Z" }, + { url = "https://files.pythonhosted.org/packages/34/36/0e730beec4d83c5306f417afbd82ff237d9a21e83c5edf675f31ed84c1fe/pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6645ce7eec4928e29a1e3b3d5c946621d105d3e79f0c9cddf07c2a9770949287", size = 2053852, upload-time = "2026-04-20T14:40:43.077Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f0/3071131f47e39136a17814576e0fada9168569f7f8c0e6ac4d1ede6a4958/pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a712c7118e6c5ea96562f7b488435172abb94a3c53c22c9efc1412264a45cbbe", size = 2221134, upload-time = "2026-04-20T14:43:03.349Z" }, + { url = "https://files.pythonhosted.org/packages/2f/a9/a2dc023eec5aa4b02a467874bad32e2446957d2adcab14e107eab502e978/pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:69a868ef3ff206343579021c40faf3b1edc64b1cc508ff243a28b0a514ccb050", size = 2279785, upload-time = "2026-04-20T14:41:19.285Z" }, + { url = "https://files.pythonhosted.org/packages/0a/44/93f489d16fb63fbd41c670441536541f6e8cfa1e5a69f40bc9c5d30d8c90/pydantic_core-2.46.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc7e8c32db809aa0f6ea1d6869ebc8518a65d5150fdfad8bcae6a49ae32a22e2", size = 2089404, upload-time = "2026-04-20T14:43:10.108Z" }, + { url = "https://files.pythonhosted.org/packages/2a/78/8692e3aa72b2d004f7a5d937f1dfdc8552ba26caf0bec75f342c40f00dec/pydantic_core-2.46.3-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:3481bd1341dc85779ee506bc8e1196a277ace359d89d28588a9468c3ecbe63fa", size = 2114898, upload-time = "2026-04-20T14:44:51.475Z" }, + { url = "https://files.pythonhosted.org/packages/6a/62/e83133f2e7832532060175cebf1f13748f4c7e7e7165cdd1f611f174494b/pydantic_core-2.46.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8690eba565c6d68ffd3a8655525cbdd5246510b44a637ee2c6c03a7ebfe64d3c", size = 2157856, upload-time = "2026-04-20T14:43:46.64Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ec/6a500e3ad7718ee50583fae79c8651f5d37e3abce1fa9ae177ae65842c53/pydantic_core-2.46.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4de88889d7e88d50d40ee5b39d5dac0bcaef9ba91f7e536ac064e6b2834ecccf", size = 2180168, upload-time = "2026-04-20T14:42:00.302Z" }, + { url = "https://files.pythonhosted.org/packages/d8/53/8267811054b1aa7fc1dc7ded93812372ef79a839f5e23558136a6afbfde1/pydantic_core-2.46.3-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:e480080975c1ef7f780b8f99ed72337e7cc5efea2e518a20a692e8e7b278eb8b", size = 2322885, upload-time = "2026-04-20T14:41:05.253Z" }, + { url = "https://files.pythonhosted.org/packages/c8/c1/1c0acdb3aa0856ddc4ecc55214578f896f2de16f400cf51627eb3c26c1c4/pydantic_core-2.46.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:de3a5c376f8cd94da9a1b8fd3dd1c16c7a7b216ed31dc8ce9fd7a22bf13b836e", size = 2360328, upload-time = "2026-04-20T14:41:43.991Z" }, + { url = "https://files.pythonhosted.org/packages/f0/d0/ef39cd0f4a926814f360e71c1adeab48ad214d9727e4deb48eedfb5bce1a/pydantic_core-2.46.3-cp311-cp311-win32.whl", hash = "sha256:fc331a5314ffddd5385b9ee9d0d2fee0b13c27e0e02dad71b1ae5d6561f51eeb", size = 1979464, upload-time = "2026-04-20T14:43:12.215Z" }, + { url = "https://files.pythonhosted.org/packages/18/9c/f41951b0d858e343f1cf09398b2a7b3014013799744f2c4a8ad6a3eec4f2/pydantic_core-2.46.3-cp311-cp311-win_amd64.whl", hash = "sha256:b5b9c6cf08a8a5e502698f5e153056d12c34b8fb30317e0c5fd06f45162a6346", size = 2070837, upload-time = "2026-04-20T14:41:47.707Z" }, + { url = "https://files.pythonhosted.org/packages/9f/1e/264a17cd582f6ed50950d4d03dd5fefd84e570e238afe1cb3e25cf238769/pydantic_core-2.46.3-cp311-cp311-win_arm64.whl", hash = "sha256:5dfd51cf457482f04ec49491811a2b8fd5b843b64b11eecd2d7a1ee596ea78a6", size = 2053647, upload-time = "2026-04-20T14:42:27.535Z" }, + { url = "https://files.pythonhosted.org/packages/4b/cb/5b47425556ecc1f3fe18ed2a0083188aa46e1dd812b06e406475b3a5d536/pydantic_core-2.46.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b11b59b3eee90a80a36701ddb4576d9ae31f93f05cb9e277ceaa09e6bf074a67", size = 2101946, upload-time = "2026-04-20T14:40:52.581Z" }, + { url = "https://files.pythonhosted.org/packages/a1/4f/2fb62c2267cae99b815bbf4a7b9283812c88ca3153ef29f7707200f1d4e5/pydantic_core-2.46.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:af8653713055ea18a3abc1537fe2ebc42f5b0bbb768d1eb79fd74eb47c0ac089", size = 1951612, upload-time = "2026-04-20T14:42:42.996Z" }, + { url = "https://files.pythonhosted.org/packages/50/6e/b7348fd30d6556d132cddd5bd79f37f96f2601fe0608afac4f5fb01ec0b3/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:75a519dab6d63c514f3a81053e5266c549679e4aa88f6ec57f2b7b854aceb1b0", size = 1977027, upload-time = "2026-04-20T14:42:02.001Z" }, + { url = "https://files.pythonhosted.org/packages/82/11/31d60ee2b45540d3fb0b29302a393dbc01cd771c473f5b5147bcd353e593/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a6cd87cb1575b1ad05ba98894c5b5c96411ef678fa2f6ed2576607095b8d9789", size = 2063008, upload-time = "2026-04-20T14:44:17.952Z" }, + { url = "https://files.pythonhosted.org/packages/8a/db/3a9d1957181b59258f44a2300ab0f0be9d1e12d662a4f57bb31250455c52/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f80a55484b8d843c8ada81ebf70a682f3f00a3d40e378c06cf17ecb44d280d7d", size = 2233082, upload-time = "2026-04-20T14:40:57.934Z" }, + { url = "https://files.pythonhosted.org/packages/9c/e1/3277c38792aeb5cfb18c2f0c5785a221d9ff4e149abbe1184d53d5f72273/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3861f1731b90c50a3266316b9044f5c9b405eecb8e299b0a7120596334e4fe9c", size = 2304615, upload-time = "2026-04-20T14:42:12.584Z" }, + { url = "https://files.pythonhosted.org/packages/5e/d5/e3d9717c9eba10855325650afd2a9cba8e607321697f18953af9d562da2f/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb528e295ed31570ac3dcc9bfdd6e0150bc11ce6168ac87a8082055cf1a67395", size = 2094380, upload-time = "2026-04-20T14:43:05.522Z" }, + { url = "https://files.pythonhosted.org/packages/a1/20/abac35dedcbfd66c6f0b03e4e3564511771d6c9b7ede10a362d03e110d9b/pydantic_core-2.46.3-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:367508faa4973b992b271ba1494acaab36eb7e8739d1e47be5035fb1ea225396", size = 2135429, upload-time = "2026-04-20T14:41:55.549Z" }, + { url = "https://files.pythonhosted.org/packages/6c/a5/41bfd1df69afad71b5cf0535055bccc73022715ad362edbc124bc1e021d7/pydantic_core-2.46.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5ad3c826fe523e4becf4fe39baa44286cff85ef137c729a2c5e269afbfd0905d", size = 2174582, upload-time = "2026-04-20T14:41:45.96Z" }, + { url = "https://files.pythonhosted.org/packages/79/65/38d86ea056b29b2b10734eb23329b7a7672ca604df4f2b6e9c02d4ee22fe/pydantic_core-2.46.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ec638c5d194ef8af27db69f16c954a09797c0dc25015ad6123eb2c73a4d271ca", size = 2187533, upload-time = "2026-04-20T14:40:55.367Z" }, + { url = "https://files.pythonhosted.org/packages/b6/55/a1129141678a2026badc539ad1dee0a71d06f54c2f06a4bd68c030ac781b/pydantic_core-2.46.3-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:28ed528c45446062ee66edb1d33df5d88828ae167de76e773a3c7f64bd14e976", size = 2332985, upload-time = "2026-04-20T14:44:13.05Z" }, + { url = "https://files.pythonhosted.org/packages/d7/60/cb26f4077719f709e54819f4e8e1d43f4091f94e285eb6bd21e1190a7b7c/pydantic_core-2.46.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aed19d0c783886d5bd86d80ae5030006b45e28464218747dcf83dabfdd092c7b", size = 2373670, upload-time = "2026-04-20T14:41:53.421Z" }, + { url = "https://files.pythonhosted.org/packages/6b/7e/c3f21882bdf1d8d086876f81b5e296206c69c6082551d776895de7801fa0/pydantic_core-2.46.3-cp312-cp312-win32.whl", hash = "sha256:06d5d8820cbbdb4147578c1fe7ffcd5b83f34508cb9f9ab76e807be7db6ff0a4", size = 1966722, upload-time = "2026-04-20T14:44:30.588Z" }, + { url = "https://files.pythonhosted.org/packages/57/be/6b5e757b859013ebfbd7adba02f23b428f37c86dcbf78b5bb0b4ffd36e99/pydantic_core-2.46.3-cp312-cp312-win_amd64.whl", hash = "sha256:c3212fda0ee959c1dd04c60b601ec31097aaa893573a3a1abd0a47bcac2968c1", size = 2072970, upload-time = "2026-04-20T14:42:54.248Z" }, + { url = "https://files.pythonhosted.org/packages/bf/f8/a989b21cc75e9a32d24192ef700eea606521221a89faa40c919ce884f2b1/pydantic_core-2.46.3-cp312-cp312-win_arm64.whl", hash = "sha256:f1f8338dd7a7f31761f1f1a3c47503a9a3b34eea3c8b01fa6ee96408affb5e72", size = 2035963, upload-time = "2026-04-20T14:44:20.4Z" }, + { url = "https://files.pythonhosted.org/packages/9b/3c/9b5e8eb9821936d065439c3b0fb1490ffa64163bfe7e1595985a47896073/pydantic_core-2.46.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:12bc98de041458b80c86c56b24df1d23832f3e166cbaff011f25d187f5c62c37", size = 2102109, upload-time = "2026-04-20T14:41:24.219Z" }, + { url = "https://files.pythonhosted.org/packages/91/97/1c41d1f5a19f241d8069f1e249853bcce378cdb76eec8ab636d7bc426280/pydantic_core-2.46.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:85348b8f89d2c3508b65b16c3c33a4da22b8215138d8b996912bb1532868885f", size = 1951820, upload-time = "2026-04-20T14:42:14.236Z" }, + { url = "https://files.pythonhosted.org/packages/30/b4/d03a7ae14571bc2b6b3c7b122441154720619afe9a336fa3a95434df5e2f/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1105677a6df914b1fb71a81b96c8cce7726857e1717d86001f29be06a25ee6f8", size = 1977785, upload-time = "2026-04-20T14:42:31.648Z" }, + { url = "https://files.pythonhosted.org/packages/ae/0c/4086f808834b59e3c8f1aa26df8f4b6d998cdcf354a143d18ef41529d1fe/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:87082cd65669a33adeba5470769e9704c7cf026cc30afb9cc77fd865578ebaad", size = 2062761, upload-time = "2026-04-20T14:40:37.093Z" }, + { url = "https://files.pythonhosted.org/packages/fa/71/a649be5a5064c2df0db06e0a512c2281134ed2fcc981f52a657936a7527c/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60e5f66e12c4f5212d08522963380eaaeac5ebd795826cfd19b2dfb0c7a52b9c", size = 2232989, upload-time = "2026-04-20T14:42:59.254Z" }, + { url = "https://files.pythonhosted.org/packages/a2/84/7756e75763e810b3a710f4724441d1ecc5883b94aacb07ca71c5fb5cfb69/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b6cdf19bf84128d5e7c37e8a73a0c5c10d51103a650ac585d42dd6ae233f2b7f", size = 2303975, upload-time = "2026-04-20T14:41:32.287Z" }, + { url = "https://files.pythonhosted.org/packages/6c/35/68a762e0c1e31f35fa0dac733cbd9f5b118042853698de9509c8e5bf128b/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:031bb17f4885a43773c8c763089499f242aee2ea85cf17154168775dccdecf35", size = 2095325, upload-time = "2026-04-20T14:42:47.685Z" }, + { url = "https://files.pythonhosted.org/packages/77/bf/1bf8c9a8e91836c926eae5e3e51dce009bf495a60ca56060689d3df3f340/pydantic_core-2.46.3-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:bcf2a8b2982a6673693eae7348ef3d8cf3979c1d63b54fca7c397a635cc68687", size = 2133368, upload-time = "2026-04-20T14:41:22.766Z" }, + { url = "https://files.pythonhosted.org/packages/e5/50/87d818d6bab915984995157ceb2380f5aac4e563dddbed6b56f0ed057aba/pydantic_core-2.46.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28e8cf2f52d72ced402a137145923a762cbb5081e48b34312f7a0c8f55928ec3", size = 2173908, upload-time = "2026-04-20T14:42:52.044Z" }, + { url = "https://files.pythonhosted.org/packages/91/88/a311fb306d0bd6185db41fa14ae888fb81d0baf648a761ae760d30819d33/pydantic_core-2.46.3-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:17eaface65d9fc5abb940003020309c1bf7a211f5f608d7870297c367e6f9022", size = 2186422, upload-time = "2026-04-20T14:43:29.55Z" }, + { url = "https://files.pythonhosted.org/packages/8f/79/28fd0d81508525ab2054fef7c77a638c8b5b0afcbbaeee493cf7c3fef7e1/pydantic_core-2.46.3-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:93fd339f23408a07e98950a89644f92c54d8729719a40b30c0a30bb9ebc55d23", size = 2332709, upload-time = "2026-04-20T14:42:16.134Z" }, + { url = "https://files.pythonhosted.org/packages/b3/21/795bf5fe5c0f379308b8ef19c50dedab2e7711dbc8d0c2acf08f1c7daa05/pydantic_core-2.46.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:23cbdb3aaa74dfe0837975dbf69b469753bbde8eacace524519ffdb6b6e89eb7", size = 2372428, upload-time = "2026-04-20T14:41:10.974Z" }, + { url = "https://files.pythonhosted.org/packages/45/b3/ed14c659cbe7605e3ef063077680a64680aec81eb1a04763a05190d49b7f/pydantic_core-2.46.3-cp313-cp313-win32.whl", hash = "sha256:610eda2e3838f401105e6326ca304f5da1e15393ae25dacae5c5c63f2c275b13", size = 1965601, upload-time = "2026-04-20T14:41:42.128Z" }, + { url = "https://files.pythonhosted.org/packages/ef/bb/adb70d9a762ddd002d723fbf1bd492244d37da41e3af7b74ad212609027e/pydantic_core-2.46.3-cp313-cp313-win_amd64.whl", hash = "sha256:68cc7866ed863db34351294187f9b729964c371ba33e31c26f478471c52e1ed0", size = 2071517, upload-time = "2026-04-20T14:43:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/52/eb/66faefabebfe68bd7788339c9c9127231e680b11906368c67ce112fdb47f/pydantic_core-2.46.3-cp313-cp313-win_arm64.whl", hash = "sha256:f64b5537ac62b231572879cd08ec05600308636a5d63bcbdb15063a466977bec", size = 2035802, upload-time = "2026-04-20T14:43:38.507Z" }, + { url = "https://files.pythonhosted.org/packages/7f/db/a7bcb4940183fda36022cd18ba8dd12f2dff40740ec7b58ce7457befa416/pydantic_core-2.46.3-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:afa3aa644f74e290cdede48a7b0bee37d1c35e71b05105f6b340d484af536d9b", size = 2097614, upload-time = "2026-04-20T14:44:38.374Z" }, + { url = "https://files.pythonhosted.org/packages/24/35/e4066358a22e3e99519db370494c7528f5a2aa1367370e80e27e20283543/pydantic_core-2.46.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ced3310e51aa425f7f77da8bbbb5212616655bedbe82c70944320bc1dbe5e018", size = 1951896, upload-time = "2026-04-20T14:40:53.996Z" }, + { url = "https://files.pythonhosted.org/packages/87/92/37cf4049d1636996e4b888c05a501f40a43ff218983a551d57f9d5e14f0d/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e29908922ce9da1a30b4da490bd1d3d82c01dcfdf864d2a74aacee674d0bfa34", size = 1979314, upload-time = "2026-04-20T14:41:49.446Z" }, + { url = "https://files.pythonhosted.org/packages/d8/36/9ff4d676dfbdfb2d591cf43f3d90ded01e15b1404fd101180ed2d62a2fd3/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0c9ff69140423eea8ed2d5477df3ba037f671f5e897d206d921bc9fdc39613e7", size = 2056133, upload-time = "2026-04-20T14:42:23.574Z" }, + { url = "https://files.pythonhosted.org/packages/bc/f0/405b442a4d7ba855b06eec8b2bf9c617d43b8432d099dfdc7bf999293495/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b675ab0a0d5b1c8fdb81195dc5bcefea3f3c240871cdd7ff9a2de8aa50772eb2", size = 2228726, upload-time = "2026-04-20T14:44:22.816Z" }, + { url = "https://files.pythonhosted.org/packages/e7/f8/65cd92dd5a0bd89ba277a98ecbfaf6fc36bbd3300973c7a4b826d6ab1391/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0087084960f209a9a4af50ecd1fb063d9ad3658c07bb81a7a53f452dacbfb2ba", size = 2301214, upload-time = "2026-04-20T14:44:48.792Z" }, + { url = "https://files.pythonhosted.org/packages/fd/86/ef96a4c6e79e7a2d0410826a68fbc0eccc0fd44aa733be199d5fcac3bb87/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed42e6cc8e1b0e2b9b96e2276bad70ae625d10d6d524aed0c93de974ae029f9f", size = 2099927, upload-time = "2026-04-20T14:41:40.196Z" }, + { url = "https://files.pythonhosted.org/packages/6d/53/269caf30e0096e0a8a8f929d1982a27b3879872cca2d917d17c2f9fdf4fe/pydantic_core-2.46.3-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:f1771ce258afb3e4201e67d154edbbae712a76a6081079fe247c2f53c6322c22", size = 2128789, upload-time = "2026-04-20T14:41:15.868Z" }, + { url = "https://files.pythonhosted.org/packages/00/b0/1a6d9b6a587e118482910c244a1c5acf4d192604174132efd12bf0ac486f/pydantic_core-2.46.3-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a7610b6a5242a6c736d8ad47fd5fff87fcfe8f833b281b1c409c3d6835d9227f", size = 2173815, upload-time = "2026-04-20T14:44:25.152Z" }, + { url = "https://files.pythonhosted.org/packages/87/56/e7e00d4041a7e62b5a40815590114db3b535bf3ca0bf4dca9f16cef25246/pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:ff5e7783bcc5476e1db448bf268f11cb257b1c276d3e89f00b5727be86dd0127", size = 2181608, upload-time = "2026-04-20T14:41:28.933Z" }, + { url = "https://files.pythonhosted.org/packages/e8/22/4bd23c3d41f7c185d60808a1de83c76cf5aeabf792f6c636a55c3b1ec7f9/pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:9d2e32edcc143bc01e95300671915d9ca052d4f745aa0a49c48d4803f8a85f2c", size = 2326968, upload-time = "2026-04-20T14:42:03.962Z" }, + { url = "https://files.pythonhosted.org/packages/24/ac/66cd45129e3915e5ade3b292cb3bc7fd537f58f8f8dbdaba6170f7cabb74/pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:6e42d83d1c6b87fa56b521479cff237e626a292f3b31b6345c15a99121b454c1", size = 2369842, upload-time = "2026-04-20T14:41:35.52Z" }, + { url = "https://files.pythonhosted.org/packages/a2/51/dd4248abb84113615473aa20d5545b7c4cd73c8644003b5259686f93996c/pydantic_core-2.46.3-cp314-cp314-win32.whl", hash = "sha256:07bc6d2a28c3adb4f7c6ae46aa4f2d2929af127f587ed44057af50bf1ce0f505", size = 1959661, upload-time = "2026-04-20T14:41:00.042Z" }, + { url = "https://files.pythonhosted.org/packages/20/eb/59980e5f1ae54a3b86372bd9f0fa373ea2d402e8cdcd3459334430f91e91/pydantic_core-2.46.3-cp314-cp314-win_amd64.whl", hash = "sha256:8940562319bc621da30714617e6a7eaa6b98c84e8c685bcdc02d7ed5e7c7c44e", size = 2071686, upload-time = "2026-04-20T14:43:16.471Z" }, + { url = "https://files.pythonhosted.org/packages/8c/db/1cf77e5247047dfee34bc01fa9bca134854f528c8eb053e144298893d370/pydantic_core-2.46.3-cp314-cp314-win_arm64.whl", hash = "sha256:5dcbbcf4d22210ced8f837c96db941bdb078f419543472aca5d9a0bb7cddc7df", size = 2026907, upload-time = "2026-04-20T14:43:31.732Z" }, + { url = "https://files.pythonhosted.org/packages/57/c0/b3df9f6a543276eadba0a48487b082ca1f201745329d97dbfa287034a230/pydantic_core-2.46.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:d0fe3dce1e836e418f912c1ad91c73357d03e556a4d286f441bf34fed2dbeecf", size = 2095047, upload-time = "2026-04-20T14:42:37.982Z" }, + { url = "https://files.pythonhosted.org/packages/66/57/886a938073b97556c168fd99e1a7305bb363cd30a6d2c76086bf0587b32a/pydantic_core-2.46.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9ce92e58abc722dac1bf835a6798a60b294e48eb0e625ec9fd994b932ac5feee", size = 1934329, upload-time = "2026-04-20T14:43:49.655Z" }, + { url = "https://files.pythonhosted.org/packages/0b/7c/b42eaa5c34b13b07ecb51da21761297a9b8eb43044c864a035999998f328/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a03e6467f0f5ab796a486146d1b887b2dc5e5f9b3288898c1b1c3ad974e53e4a", size = 1974847, upload-time = "2026-04-20T14:42:10.737Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9b/92b42db6543e7de4f99ae977101a2967b63122d4b6cf7773812da2d7d5b5/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2798b6ba041b9d70acfb9071a2ea13c8456dd1e6a5555798e41ba7b0790e329c", size = 2041742, upload-time = "2026-04-20T14:40:44.262Z" }, + { url = "https://files.pythonhosted.org/packages/0f/19/46fbe1efabb5aa2834b43b9454e70f9a83ad9c338c1291e48bdc4fecf167/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9be3e221bdc6d69abf294dcf7aff6af19c31a5cdcc8f0aa3b14be29df4bd03b1", size = 2236235, upload-time = "2026-04-20T14:41:27.307Z" }, + { url = "https://files.pythonhosted.org/packages/77/da/b3f95bc009ad60ec53120f5d16c6faa8cabdbe8a20d83849a1f2b8728148/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f13936129ce841f2a5ddf6f126fea3c43cd128807b5a59588c37cf10178c2e64", size = 2282633, upload-time = "2026-04-20T14:44:33.271Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6e/401336117722e28f32fb8220df676769d28ebdf08f2f4469646d404c43a3/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28b5f2ef03416facccb1c6ef744c69793175fd27e44ef15669201601cf423acb", size = 2109679, upload-time = "2026-04-20T14:44:41.065Z" }, + { url = "https://files.pythonhosted.org/packages/fc/53/b289f9bc8756a32fe718c46f55afaeaf8d489ee18d1a1e7be1db73f42cc4/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:830d1247d77ad23852314f069e9d7ddafeec5f684baf9d7e7065ed46a049c4e6", size = 2108342, upload-time = "2026-04-20T14:42:50.144Z" }, + { url = "https://files.pythonhosted.org/packages/10/5b/8292fc7c1f9111f1b2b7c1b0dcf1179edcd014fc3ea4517499f50b829d71/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0793c90c1a3c74966e7975eaef3ed30ebdff3260a0f815a62a22adc17e4c01c", size = 2157208, upload-time = "2026-04-20T14:42:08.133Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9e/f80044e9ec07580f057a89fc131f78dda7a58751ddf52bbe05eaf31db50f/pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:d2d0aead851b66f5245ec0c4fb2612ef457f8bbafefdf65a2bf9d6bac6140f47", size = 2167237, upload-time = "2026-04-20T14:42:25.412Z" }, + { url = "https://files.pythonhosted.org/packages/f8/84/6781a1b037f3b96be9227edbd1101f6d3946746056231bf4ac48cdff1a8d/pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:2f40e4246676beb31c5ce77c38a55ca4e465c6b38d11ea1bd935420568e0b1ab", size = 2312540, upload-time = "2026-04-20T14:40:40.313Z" }, + { url = "https://files.pythonhosted.org/packages/3e/db/19c0839feeb728e7df03255581f198dfdf1c2aeb1e174a8420b63c5252e5/pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:cf489cf8986c543939aeee17a09c04d6ffb43bfef8ca16fcbcc5cfdcbed24dba", size = 2369556, upload-time = "2026-04-20T14:41:09.427Z" }, + { url = "https://files.pythonhosted.org/packages/e0/15/3228774cb7cd45f5f721ddf1b2242747f4eb834d0c491f0c02d606f09fed/pydantic_core-2.46.3-cp314-cp314t-win32.whl", hash = "sha256:ffe0883b56cfc05798bf994164d2b2ff03efe2d22022a2bb080f3b626176dd56", size = 1949756, upload-time = "2026-04-20T14:41:25.717Z" }, + { url = "https://files.pythonhosted.org/packages/b8/2a/c79cf53fd91e5a87e30d481809f52f9a60dd221e39de66455cf04deaad37/pydantic_core-2.46.3-cp314-cp314t-win_amd64.whl", hash = "sha256:706d9d0ce9cf4593d07270d8e9f53b161f90c57d315aeec4fb4fd7a8b10240d8", size = 2051305, upload-time = "2026-04-20T14:43:18.627Z" }, + { url = "https://files.pythonhosted.org/packages/0b/db/d8182a7f1d9343a032265aae186eb063fe26ca4c40f256b21e8da4498e89/pydantic_core-2.46.3-cp314-cp314t-win_arm64.whl", hash = "sha256:77706aeb41df6a76568434701e0917da10692da28cb69d5fb6919ce5fdb07374", size = 2026310, upload-time = "2026-04-20T14:41:01.778Z" }, + { url = "https://files.pythonhosted.org/packages/66/7f/03dbad45cd3aa9083fbc93c210ae8b005af67e4136a14186950a747c6874/pydantic_core-2.46.3-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:9715525891ed524a0a1eb6d053c74d4d4ad5017677fb00af0b7c2644a31bae46", size = 2105683, upload-time = "2026-04-20T14:42:19.779Z" }, + { url = "https://files.pythonhosted.org/packages/26/22/4dc186ac8ea6b257e9855031f51b62a9637beac4d68ac06bee02f046f836/pydantic_core-2.46.3-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:9d2f400712a99a013aff420ef1eb9be077f8189a36c1e3ef87660b4e1088a874", size = 1940052, upload-time = "2026-04-20T14:43:59.274Z" }, + { url = "https://files.pythonhosted.org/packages/0d/ca/d376391a5aff1f2e8188960d7873543608130a870961c2b6b5236627c116/pydantic_core-2.46.3-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd2aab0e2e9dc2daf36bd2686c982535d5e7b1d930a1344a7bb6e82baab42a76", size = 1988172, upload-time = "2026-04-20T14:41:17.469Z" }, + { url = "https://files.pythonhosted.org/packages/0e/6b/523b9f85c23788755d6ab949329de692a2e3a584bc6beb67fef5e035aa9d/pydantic_core-2.46.3-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e9d76736da5f362fabfeea6a69b13b7f2be405c6d6966f06b2f6bfff7e64531", size = 2128596, upload-time = "2026-04-20T14:40:41.707Z" }, + { url = "https://files.pythonhosted.org/packages/34/42/f426db557e8ab2791bc7562052299944a118655496fbff99914e564c0a94/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:b12dd51f1187c2eb489af8e20f880362db98e954b54ab792fa5d92e8bcc6b803", size = 2091877, upload-time = "2026-04-20T14:43:27.091Z" }, + { url = "https://files.pythonhosted.org/packages/5c/4f/86a832a9d14df58e663bfdf4627dc00d3317c2bd583c4fb23390b0f04b8e/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:f00a0961b125f1a47af7bcc17f00782e12f4cd056f83416006b30111d941dfa3", size = 1932428, upload-time = "2026-04-20T14:40:45.781Z" }, + { url = "https://files.pythonhosted.org/packages/11/1a/fe857968954d93fb78e0d4b6df5c988c74c4aaa67181c60be7cfe327c0ca/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57697d7c056aca4bbb680200f96563e841a6386ac1129370a0102592f4dddff5", size = 1997550, upload-time = "2026-04-20T14:44:02.425Z" }, + { url = "https://files.pythonhosted.org/packages/17/eb/9d89ad2d9b0ba8cd65393d434471621b98912abb10fbe1df08e480ba57b5/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd35aa21299def8db7ef4fe5c4ff862941a9a158ca7b63d61e66fe67d30416b4", size = 2137657, upload-time = "2026-04-20T14:42:45.149Z" }, + { url = "https://files.pythonhosted.org/packages/1f/da/99d40830684f81dec901cac521b5b91c095394cc1084b9433393cde1c2df/pydantic_core-2.46.3-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:13afdd885f3d71280cf286b13b310ee0f7ccfefd1dbbb661514a474b726e2f25", size = 2107973, upload-time = "2026-04-20T14:42:06.175Z" }, + { url = "https://files.pythonhosted.org/packages/99/a5/87024121818d75bbb2a98ddbaf638e40e7a18b5e0f5492c9ca4b1b316107/pydantic_core-2.46.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f91c0aff3e3ee0928edd1232c57f643a7a003e6edf1860bc3afcdc749cb513f3", size = 1947191, upload-time = "2026-04-20T14:43:14.319Z" }, + { url = "https://files.pythonhosted.org/packages/60/62/0c1acfe10945b83a6a59d19fbaa92f48825381509e5701b855c08f13db76/pydantic_core-2.46.3-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6529d1d128321a58d30afcc97b49e98836542f68dd41b33c2e972bb9e5290536", size = 2123791, upload-time = "2026-04-20T14:43:22.766Z" }, + { url = "https://files.pythonhosted.org/packages/75/3e/3b2393b4c8f44285561dc30b00cf307a56a2eff7c483a824db3b8221ca51/pydantic_core-2.46.3-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:975c267cff4f7e7272eacbe50f6cc03ca9a3da4c4fbd66fffd89c94c1e311aa1", size = 2153197, upload-time = "2026-04-20T14:44:27.932Z" }, + { url = "https://files.pythonhosted.org/packages/ba/75/5af02fb35505051eee727c061f2881c555ab4f8ddb2d42da715a42c9731b/pydantic_core-2.46.3-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:2b8e4f2bbdf71415c544b4b1138b8060db7b6611bc927e8064c769f64bed651c", size = 2181073, upload-time = "2026-04-20T14:43:20.729Z" }, + { url = "https://files.pythonhosted.org/packages/10/92/7e0e1bd9ca3c68305db037560ca2876f89b2647deb2f8b6319005de37505/pydantic_core-2.46.3-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:e61ea8e9fff9606d09178f577ff8ccdd7206ff73d6552bcec18e1033c4254b85", size = 2315886, upload-time = "2026-04-20T14:44:04.826Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d8/101655f27eaf3e44558ead736b2795d12500598beed4683f279396fa186e/pydantic_core-2.46.3-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b504bda01bafc69b6d3c7a0c7f039dcf60f47fab70e06fe23f57b5c75bdc82b8", size = 2360528, upload-time = "2026-04-20T14:40:47.431Z" }, + { url = "https://files.pythonhosted.org/packages/07/0f/1c34a74c8d07136f0d729ffe5e1fdab04fbdaa7684f61a92f92511a84a15/pydantic_core-2.46.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:b00b76f7142fc60c762ce579bd29c8fa44aaa56592dd3c54fab3928d0d4ca6ff", size = 2184144, upload-time = "2026-04-20T14:42:57Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[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, upload-time = "2024-03-01T18:36:20.211Z" } +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, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "python-ulid" +version = "3.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/40/7e/0d6c82b5ccc71e7c833aed43d9e8468e1f2ff0be1b3f657a6fcafbb8433d/python_ulid-3.1.0.tar.gz", hash = "sha256:ff0410a598bc5f6b01b602851a3296ede6f91389f913a5d5f8c496003836f636", size = 93175, upload-time = "2025-08-18T16:09:26.305Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6c/a0/4ed6632b70a52de845df056654162acdebaf97c20e3212c559ac43e7216e/python_ulid-3.1.0-py3-none-any.whl", hash = "sha256:e2cdc979c8c877029b4b7a38a6fba3bc4578e4f109a308419ff4d3ccf0a46619", size = 11577, upload-time = "2025-08-18T16:09:25.047Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "pyzmq" +version = "27.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "implementation_name == 'pypy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/0b/3c9baedbdf613ecaa7aa07027780b8867f57b6293b6ee50de316c9f3222b/pyzmq-27.1.0.tar.gz", hash = "sha256:ac0765e3d44455adb6ddbf4417dcce460fc40a05978c08efdf2948072f6db540", size = 281750, upload-time = "2025-09-08T23:10:18.157Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/5d/305323ba86b284e6fcb0d842d6adaa2999035f70f8c38a9b6d21ad28c3d4/pyzmq-27.1.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:226b091818d461a3bef763805e75685e478ac17e9008f49fce2d3e52b3d58b86", size = 1333328, upload-time = "2025-09-08T23:07:45.946Z" }, + { url = "https://files.pythonhosted.org/packages/bd/a0/fc7e78a23748ad5443ac3275943457e8452da67fda347e05260261108cbc/pyzmq-27.1.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:0790a0161c281ca9723f804871b4027f2e8b5a528d357c8952d08cd1a9c15581", size = 908803, upload-time = "2025-09-08T23:07:47.551Z" }, + { url = "https://files.pythonhosted.org/packages/7e/22/37d15eb05f3bdfa4abea6f6d96eb3bb58585fbd3e4e0ded4e743bc650c97/pyzmq-27.1.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c895a6f35476b0c3a54e3eb6ccf41bf3018de937016e6e18748317f25d4e925f", size = 668836, upload-time = "2025-09-08T23:07:49.436Z" }, + { url = "https://files.pythonhosted.org/packages/b1/c4/2a6fe5111a01005fc7af3878259ce17684fabb8852815eda6225620f3c59/pyzmq-27.1.0-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bbf8d3630bf96550b3be8e1fc0fea5cbdc8d5466c1192887bd94869da17a63e", size = 857038, upload-time = "2025-09-08T23:07:51.234Z" }, + { url = "https://files.pythonhosted.org/packages/cb/eb/bfdcb41d0db9cd233d6fb22dc131583774135505ada800ebf14dfb0a7c40/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:15c8bd0fe0dabf808e2d7a681398c4e5ded70a551ab47482067a572c054c8e2e", size = 1657531, upload-time = "2025-09-08T23:07:52.795Z" }, + { url = "https://files.pythonhosted.org/packages/ab/21/e3180ca269ed4a0de5c34417dfe71a8ae80421198be83ee619a8a485b0c7/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:bafcb3dd171b4ae9f19ee6380dfc71ce0390fefaf26b504c0e5f628d7c8c54f2", size = 2034786, upload-time = "2025-09-08T23:07:55.047Z" }, + { url = "https://files.pythonhosted.org/packages/3b/b1/5e21d0b517434b7f33588ff76c177c5a167858cc38ef740608898cd329f2/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e829529fcaa09937189178115c49c504e69289abd39967cd8a4c215761373394", size = 1894220, upload-time = "2025-09-08T23:07:57.172Z" }, + { url = "https://files.pythonhosted.org/packages/03/f2/44913a6ff6941905efc24a1acf3d3cb6146b636c546c7406c38c49c403d4/pyzmq-27.1.0-cp311-cp311-win32.whl", hash = "sha256:6df079c47d5902af6db298ec92151db82ecb557af663098b92f2508c398bb54f", size = 567155, upload-time = "2025-09-08T23:07:59.05Z" }, + { url = "https://files.pythonhosted.org/packages/23/6d/d8d92a0eb270a925c9b4dd039c0b4dc10abc2fcbc48331788824ef113935/pyzmq-27.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:190cbf120fbc0fc4957b56866830def56628934a9d112aec0e2507aa6a032b97", size = 633428, upload-time = "2025-09-08T23:08:00.663Z" }, + { url = "https://files.pythonhosted.org/packages/ae/14/01afebc96c5abbbd713ecfc7469cfb1bc801c819a74ed5c9fad9a48801cb/pyzmq-27.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:eca6b47df11a132d1745eb3b5b5e557a7dae2c303277aa0e69c6ba91b8736e07", size = 559497, upload-time = "2025-09-08T23:08:02.15Z" }, + { url = "https://files.pythonhosted.org/packages/92/e7/038aab64a946d535901103da16b953c8c9cc9c961dadcbf3609ed6428d23/pyzmq-27.1.0-cp312-abi3-macosx_10_15_universal2.whl", hash = "sha256:452631b640340c928fa343801b0d07eb0c3789a5ffa843f6e1a9cee0ba4eb4fc", size = 1306279, upload-time = "2025-09-08T23:08:03.807Z" }, + { url = "https://files.pythonhosted.org/packages/e8/5e/c3c49fdd0f535ef45eefcc16934648e9e59dace4a37ee88fc53f6cd8e641/pyzmq-27.1.0-cp312-abi3-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1c179799b118e554b66da67d88ed66cd37a169f1f23b5d9f0a231b4e8d44a113", size = 895645, upload-time = "2025-09-08T23:08:05.301Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e5/b0b2504cb4e903a74dcf1ebae157f9e20ebb6ea76095f6cfffea28c42ecd/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3837439b7f99e60312f0c926a6ad437b067356dc2bc2ec96eb395fd0fe804233", size = 652574, upload-time = "2025-09-08T23:08:06.828Z" }, + { url = "https://files.pythonhosted.org/packages/f8/9b/c108cdb55560eaf253f0cbdb61b29971e9fb34d9c3499b0e96e4e60ed8a5/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43ad9a73e3da1fab5b0e7e13402f0b2fb934ae1c876c51d0afff0e7c052eca31", size = 840995, upload-time = "2025-09-08T23:08:08.396Z" }, + { url = "https://files.pythonhosted.org/packages/c2/bb/b79798ca177b9eb0825b4c9998c6af8cd2a7f15a6a1a4272c1d1a21d382f/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0de3028d69d4cdc475bfe47a6128eb38d8bc0e8f4d69646adfbcd840facbac28", size = 1642070, upload-time = "2025-09-08T23:08:09.989Z" }, + { url = "https://files.pythonhosted.org/packages/9c/80/2df2e7977c4ede24c79ae39dcef3899bfc5f34d1ca7a5b24f182c9b7a9ca/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_i686.whl", hash = "sha256:cf44a7763aea9298c0aa7dbf859f87ed7012de8bda0f3977b6fb1d96745df856", size = 2021121, upload-time = "2025-09-08T23:08:11.907Z" }, + { url = "https://files.pythonhosted.org/packages/46/bd/2d45ad24f5f5ae7e8d01525eb76786fa7557136555cac7d929880519e33a/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f30f395a9e6fbca195400ce833c731e7b64c3919aa481af4d88c3759e0cb7496", size = 1878550, upload-time = "2025-09-08T23:08:13.513Z" }, + { url = "https://files.pythonhosted.org/packages/e6/2f/104c0a3c778d7c2ab8190e9db4f62f0b6957b53c9d87db77c284b69f33ea/pyzmq-27.1.0-cp312-abi3-win32.whl", hash = "sha256:250e5436a4ba13885494412b3da5d518cd0d3a278a1ae640e113c073a5f88edd", size = 559184, upload-time = "2025-09-08T23:08:15.163Z" }, + { url = "https://files.pythonhosted.org/packages/fc/7f/a21b20d577e4100c6a41795842028235998a643b1ad406a6d4163ea8f53e/pyzmq-27.1.0-cp312-abi3-win_amd64.whl", hash = "sha256:9ce490cf1d2ca2ad84733aa1d69ce6855372cb5ce9223802450c9b2a7cba0ccf", size = 619480, upload-time = "2025-09-08T23:08:17.192Z" }, + { url = "https://files.pythonhosted.org/packages/78/c2/c012beae5f76b72f007a9e91ee9401cb88c51d0f83c6257a03e785c81cc2/pyzmq-27.1.0-cp312-abi3-win_arm64.whl", hash = "sha256:75a2f36223f0d535a0c919e23615fc85a1e23b71f40c7eb43d7b1dedb4d8f15f", size = 552993, upload-time = "2025-09-08T23:08:18.926Z" }, + { url = "https://files.pythonhosted.org/packages/60/cb/84a13459c51da6cec1b7b1dc1a47e6db6da50b77ad7fd9c145842750a011/pyzmq-27.1.0-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:93ad4b0855a664229559e45c8d23797ceac03183c7b6f5b4428152a6b06684a5", size = 1122436, upload-time = "2025-09-08T23:08:20.801Z" }, + { url = "https://files.pythonhosted.org/packages/dc/b6/94414759a69a26c3dd674570a81813c46a078767d931a6c70ad29fc585cb/pyzmq-27.1.0-cp313-cp313-android_24_x86_64.whl", hash = "sha256:fbb4f2400bfda24f12f009cba62ad5734148569ff4949b1b6ec3b519444342e6", size = 1156301, upload-time = "2025-09-08T23:08:22.47Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ad/15906493fd40c316377fd8a8f6b1f93104f97a752667763c9b9c1b71d42d/pyzmq-27.1.0-cp313-cp313t-macosx_10_15_universal2.whl", hash = "sha256:e343d067f7b151cfe4eb3bb796a7752c9d369eed007b91231e817071d2c2fec7", size = 1341197, upload-time = "2025-09-08T23:08:24.286Z" }, + { url = "https://files.pythonhosted.org/packages/14/1d/d343f3ce13db53a54cb8946594e567410b2125394dafcc0268d8dda027e0/pyzmq-27.1.0-cp313-cp313t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:08363b2011dec81c354d694bdecaef4770e0ae96b9afea70b3f47b973655cc05", size = 897275, upload-time = "2025-09-08T23:08:26.063Z" }, + { url = "https://files.pythonhosted.org/packages/69/2d/d83dd6d7ca929a2fc67d2c3005415cdf322af7751d773524809f9e585129/pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d54530c8c8b5b8ddb3318f481297441af102517602b569146185fa10b63f4fa9", size = 660469, upload-time = "2025-09-08T23:08:27.623Z" }, + { url = "https://files.pythonhosted.org/packages/3e/cd/9822a7af117f4bc0f1952dbe9ef8358eb50a24928efd5edf54210b850259/pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f3afa12c392f0a44a2414056d730eebc33ec0926aae92b5ad5cf26ebb6cc128", size = 847961, upload-time = "2025-09-08T23:08:29.672Z" }, + { url = "https://files.pythonhosted.org/packages/9a/12/f003e824a19ed73be15542f172fd0ec4ad0b60cf37436652c93b9df7c585/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c65047adafe573ff023b3187bb93faa583151627bc9c51fc4fb2c561ed689d39", size = 1650282, upload-time = "2025-09-08T23:08:31.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4a/e82d788ed58e9a23995cee70dbc20c9aded3d13a92d30d57ec2291f1e8a3/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:90e6e9441c946a8b0a667356f7078d96411391a3b8f80980315455574177ec97", size = 2024468, upload-time = "2025-09-08T23:08:33.543Z" }, + { url = "https://files.pythonhosted.org/packages/d9/94/2da0a60841f757481e402b34bf4c8bf57fa54a5466b965de791b1e6f747d/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:add071b2d25f84e8189aaf0882d39a285b42fa3853016ebab234a5e78c7a43db", size = 1885394, upload-time = "2025-09-08T23:08:35.51Z" }, + { url = "https://files.pythonhosted.org/packages/4f/6f/55c10e2e49ad52d080dc24e37adb215e5b0d64990b57598abc2e3f01725b/pyzmq-27.1.0-cp313-cp313t-win32.whl", hash = "sha256:7ccc0700cfdf7bd487bea8d850ec38f204478681ea02a582a8da8171b7f90a1c", size = 574964, upload-time = "2025-09-08T23:08:37.178Z" }, + { url = "https://files.pythonhosted.org/packages/87/4d/2534970ba63dd7c522d8ca80fb92777f362c0f321900667c615e2067cb29/pyzmq-27.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:8085a9fba668216b9b4323be338ee5437a235fe275b9d1610e422ccc279733e2", size = 641029, upload-time = "2025-09-08T23:08:40.595Z" }, + { url = "https://files.pythonhosted.org/packages/f6/fa/f8aea7a28b0641f31d40dea42d7ef003fded31e184ef47db696bc74cd610/pyzmq-27.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:6bb54ca21bcfe361e445256c15eedf083f153811c37be87e0514934d6913061e", size = 561541, upload-time = "2025-09-08T23:08:42.668Z" }, + { url = "https://files.pythonhosted.org/packages/87/45/19efbb3000956e82d0331bafca5d9ac19ea2857722fa2caacefb6042f39d/pyzmq-27.1.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ce980af330231615756acd5154f29813d553ea555485ae712c491cd483df6b7a", size = 1341197, upload-time = "2025-09-08T23:08:44.973Z" }, + { url = "https://files.pythonhosted.org/packages/48/43/d72ccdbf0d73d1343936296665826350cb1e825f92f2db9db3e61c2162a2/pyzmq-27.1.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1779be8c549e54a1c38f805e56d2a2e5c009d26de10921d7d51cfd1c8d4632ea", size = 897175, upload-time = "2025-09-08T23:08:46.601Z" }, + { url = "https://files.pythonhosted.org/packages/2f/2e/a483f73a10b65a9ef0161e817321d39a770b2acf8bcf3004a28d90d14a94/pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7200bb0f03345515df50d99d3db206a0a6bee1955fbb8c453c76f5bf0e08fb96", size = 660427, upload-time = "2025-09-08T23:08:48.187Z" }, + { url = "https://files.pythonhosted.org/packages/f5/d2/5f36552c2d3e5685abe60dfa56f91169f7a2d99bbaf67c5271022ab40863/pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01c0e07d558b06a60773744ea6251f769cd79a41a97d11b8bf4ab8f034b0424d", size = 847929, upload-time = "2025-09-08T23:08:49.76Z" }, + { url = "https://files.pythonhosted.org/packages/c4/2a/404b331f2b7bf3198e9945f75c4c521f0c6a3a23b51f7a4a401b94a13833/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:80d834abee71f65253c91540445d37c4c561e293ba6e741b992f20a105d69146", size = 1650193, upload-time = "2025-09-08T23:08:51.7Z" }, + { url = "https://files.pythonhosted.org/packages/1c/0b/f4107e33f62a5acf60e3ded67ed33d79b4ce18de432625ce2fc5093d6388/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:544b4e3b7198dde4a62b8ff6685e9802a9a1ebf47e77478a5eb88eca2a82f2fd", size = 2024388, upload-time = "2025-09-08T23:08:53.393Z" }, + { url = "https://files.pythonhosted.org/packages/0d/01/add31fe76512642fd6e40e3a3bd21f4b47e242c8ba33efb6809e37076d9b/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cedc4c68178e59a4046f97eca31b148ddcf51e88677de1ef4e78cf06c5376c9a", size = 1885316, upload-time = "2025-09-08T23:08:55.702Z" }, + { url = "https://files.pythonhosted.org/packages/c4/59/a5f38970f9bf07cee96128de79590bb354917914a9be11272cfc7ff26af0/pyzmq-27.1.0-cp314-cp314t-win32.whl", hash = "sha256:1f0b2a577fd770aa6f053211a55d1c47901f4d537389a034c690291485e5fe92", size = 587472, upload-time = "2025-09-08T23:08:58.18Z" }, + { url = "https://files.pythonhosted.org/packages/70/d8/78b1bad170f93fcf5e3536e70e8fadac55030002275c9a29e8f5719185de/pyzmq-27.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:19c9468ae0437f8074af379e986c5d3d7d7bfe033506af442e8c879732bedbe0", size = 661401, upload-time = "2025-09-08T23:08:59.802Z" }, + { url = "https://files.pythonhosted.org/packages/81/d6/4bfbb40c9a0b42fc53c7cf442f6385db70b40f74a783130c5d0a5aa62228/pyzmq-27.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dc5dbf68a7857b59473f7df42650c621d7e8923fb03fa74a526890f4d33cc4d7", size = 575170, upload-time = "2025-09-08T23:09:01.418Z" }, + { url = "https://files.pythonhosted.org/packages/4c/c6/c4dcdecdbaa70969ee1fdced6d7b8f60cfabe64d25361f27ac4665a70620/pyzmq-27.1.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:18770c8d3563715387139060d37859c02ce40718d1faf299abddcdcc6a649066", size = 836265, upload-time = "2025-09-08T23:09:49.376Z" }, + { url = "https://files.pythonhosted.org/packages/3e/79/f38c92eeaeb03a2ccc2ba9866f0439593bb08c5e3b714ac1d553e5c96e25/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:ac25465d42f92e990f8d8b0546b01c391ad431c3bf447683fdc40565941d0604", size = 800208, upload-time = "2025-09-08T23:09:51.073Z" }, + { url = "https://files.pythonhosted.org/packages/49/0e/3f0d0d335c6b3abb9b7b723776d0b21fa7f3a6c819a0db6097059aada160/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53b40f8ae006f2734ee7608d59ed661419f087521edbfc2149c3932e9c14808c", size = 567747, upload-time = "2025-09-08T23:09:52.698Z" }, + { url = "https://files.pythonhosted.org/packages/a1/cf/f2b3784d536250ffd4be70e049f3b60981235d70c6e8ce7e3ef21e1adb25/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f605d884e7c8be8fe1aa94e0a783bf3f591b84c24e4bc4f3e7564c82ac25e271", size = 747371, upload-time = "2025-09-08T23:09:54.563Z" }, + { url = "https://files.pythonhosted.org/packages/01/1b/5dbe84eefc86f48473947e2f41711aded97eecef1231f4558f1f02713c12/pyzmq-27.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c9f7f6e13dff2e44a6afeaf2cf54cee5929ad64afaf4d40b50f93c58fc687355", size = 544862, upload-time = "2025-09-08T23:09:56.509Z" }, +] + +[[package]] +name = "referencing" +version = "0.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, +] + +[[package]] +name = "resources-sample" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "netrun" }, +] + +[package.metadata] +requires-dist = [{ name = "netrun", editable = "../../netrun" }] + +[[package]] +name = "rich" +version = "15.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, +] + +[[package]] +name = "rpds-py" +version = "0.30.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/6e/f964e88b3d2abee2a82c1ac8366da848fce1c6d834dc2132c3fda3970290/rpds_py-0.30.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425", size = 370157, upload-time = "2025-11-30T20:21:53.789Z" }, + { url = "https://files.pythonhosted.org/packages/94/ba/24e5ebb7c1c82e74c4e4f33b2112a5573ddc703915b13a073737b59b86e0/rpds_py-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d", size = 359676, upload-time = "2025-11-30T20:21:55.475Z" }, + { url = "https://files.pythonhosted.org/packages/84/86/04dbba1b087227747d64d80c3b74df946b986c57af0a9f0c98726d4d7a3b/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4", size = 389938, upload-time = "2025-11-30T20:21:57.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/bb/1463f0b1722b7f45431bdd468301991d1328b16cffe0b1c2918eba2c4eee/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f", size = 402932, upload-time = "2025-11-30T20:21:58.47Z" }, + { url = "https://files.pythonhosted.org/packages/99/ee/2520700a5c1f2d76631f948b0736cdf9b0acb25abd0ca8e889b5c62ac2e3/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4", size = 525830, upload-time = "2025-11-30T20:21:59.699Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ad/bd0331f740f5705cc555a5e17fdf334671262160270962e69a2bdef3bf76/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97", size = 412033, upload-time = "2025-11-30T20:22:00.991Z" }, + { url = "https://files.pythonhosted.org/packages/f8/1e/372195d326549bb51f0ba0f2ecb9874579906b97e08880e7a65c3bef1a99/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89", size = 390828, upload-time = "2025-11-30T20:22:02.723Z" }, + { url = "https://files.pythonhosted.org/packages/ab/2b/d88bb33294e3e0c76bc8f351a3721212713629ffca1700fa94979cb3eae8/rpds_py-0.30.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d", size = 404683, upload-time = "2025-11-30T20:22:04.367Z" }, + { url = "https://files.pythonhosted.org/packages/50/32/c759a8d42bcb5289c1fac697cd92f6fe01a018dd937e62ae77e0e7f15702/rpds_py-0.30.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038", size = 421583, upload-time = "2025-11-30T20:22:05.814Z" }, + { url = "https://files.pythonhosted.org/packages/2b/81/e729761dbd55ddf5d84ec4ff1f47857f4374b0f19bdabfcf929164da3e24/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7", size = 572496, upload-time = "2025-11-30T20:22:07.713Z" }, + { url = "https://files.pythonhosted.org/packages/14/f6/69066a924c3557c9c30baa6ec3a0aa07526305684c6f86c696b08860726c/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed", size = 598669, upload-time = "2025-11-30T20:22:09.312Z" }, + { url = "https://files.pythonhosted.org/packages/5f/48/905896b1eb8a05630d20333d1d8ffd162394127b74ce0b0784ae04498d32/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85", size = 561011, upload-time = "2025-11-30T20:22:11.309Z" }, + { url = "https://files.pythonhosted.org/packages/22/16/cd3027c7e279d22e5eb431dd3c0fbc677bed58797fe7581e148f3f68818b/rpds_py-0.30.0-cp311-cp311-win32.whl", hash = "sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c", size = 221406, upload-time = "2025-11-30T20:22:13.101Z" }, + { url = "https://files.pythonhosted.org/packages/fa/5b/e7b7aa136f28462b344e652ee010d4de26ee9fd16f1bfd5811f5153ccf89/rpds_py-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825", size = 236024, upload-time = "2025-11-30T20:22:14.853Z" }, + { url = "https://files.pythonhosted.org/packages/14/a6/364bba985e4c13658edb156640608f2c9e1d3ea3c81b27aa9d889fff0e31/rpds_py-0.30.0-cp311-cp311-win_arm64.whl", hash = "sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229", size = 229069, upload-time = "2025-11-30T20:22:16.577Z" }, + { url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086, upload-time = "2025-11-30T20:22:17.93Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053, upload-time = "2025-11-30T20:22:19.297Z" }, + { url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763, upload-time = "2025-11-30T20:22:21.661Z" }, + { url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951, upload-time = "2025-11-30T20:22:23.408Z" }, + { url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622, upload-time = "2025-11-30T20:22:25.16Z" }, + { url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492, upload-time = "2025-11-30T20:22:26.505Z" }, + { url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080, upload-time = "2025-11-30T20:22:27.934Z" }, + { url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680, upload-time = "2025-11-30T20:22:29.341Z" }, + { url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589, upload-time = "2025-11-30T20:22:31.469Z" }, + { url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289, upload-time = "2025-11-30T20:22:32.997Z" }, + { url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737, upload-time = "2025-11-30T20:22:34.419Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120, upload-time = "2025-11-30T20:22:35.903Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782, upload-time = "2025-11-30T20:22:37.271Z" }, + { url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463, upload-time = "2025-11-30T20:22:39.021Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868, upload-time = "2025-11-30T20:22:40.493Z" }, + { url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887, upload-time = "2025-11-30T20:22:41.812Z" }, + { url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904, upload-time = "2025-11-30T20:22:43.479Z" }, + { url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945, upload-time = "2025-11-30T20:22:44.819Z" }, + { url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783, upload-time = "2025-11-30T20:22:46.103Z" }, + { url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021, upload-time = "2025-11-30T20:22:47.458Z" }, + { url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589, upload-time = "2025-11-30T20:22:48.872Z" }, + { url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025, upload-time = "2025-11-30T20:22:50.196Z" }, + { url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895, upload-time = "2025-11-30T20:22:51.87Z" }, + { url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799, upload-time = "2025-11-30T20:22:53.341Z" }, + { url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731, upload-time = "2025-11-30T20:22:54.778Z" }, + { url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027, upload-time = "2025-11-30T20:22:56.212Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020, upload-time = "2025-11-30T20:22:58.2Z" }, + { url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139, upload-time = "2025-11-30T20:23:00.209Z" }, + { url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224, upload-time = "2025-11-30T20:23:02.008Z" }, + { url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645, upload-time = "2025-11-30T20:23:03.43Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443, upload-time = "2025-11-30T20:23:04.878Z" }, + { url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375, upload-time = "2025-11-30T20:23:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850, upload-time = "2025-11-30T20:23:07.825Z" }, + { url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812, upload-time = "2025-11-30T20:23:09.228Z" }, + { url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841, upload-time = "2025-11-30T20:23:11.186Z" }, + { url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149, upload-time = "2025-11-30T20:23:12.864Z" }, + { url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843, upload-time = "2025-11-30T20:23:14.638Z" }, + { url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507, upload-time = "2025-11-30T20:23:16.105Z" }, + { url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949, upload-time = "2025-11-30T20:23:17.539Z" }, + { url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790, upload-time = "2025-11-30T20:23:19.029Z" }, + { url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217, upload-time = "2025-11-30T20:23:20.885Z" }, + { url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" }, + { url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341, upload-time = "2025-11-30T20:23:24.449Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768, upload-time = "2025-11-30T20:23:25.908Z" }, + { url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099, upload-time = "2025-11-30T20:23:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192, upload-time = "2025-11-30T20:23:29.151Z" }, + { url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080, upload-time = "2025-11-30T20:23:30.785Z" }, + { url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841, upload-time = "2025-11-30T20:23:32.209Z" }, + { url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670, upload-time = "2025-11-30T20:23:33.742Z" }, + { url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005, upload-time = "2025-11-30T20:23:35.253Z" }, + { url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112, upload-time = "2025-11-30T20:23:36.842Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049, upload-time = "2025-11-30T20:23:38.343Z" }, + { url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661, upload-time = "2025-11-30T20:23:40.263Z" }, + { url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606, upload-time = "2025-11-30T20:23:42.186Z" }, + { url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126, upload-time = "2025-11-30T20:23:44.086Z" }, + { url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371, upload-time = "2025-11-30T20:23:46.004Z" }, + { url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298, upload-time = "2025-11-30T20:23:47.696Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604, upload-time = "2025-11-30T20:23:49.501Z" }, + { url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391, upload-time = "2025-11-30T20:23:50.96Z" }, + { url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868, upload-time = "2025-11-30T20:23:52.494Z" }, + { url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747, upload-time = "2025-11-30T20:23:54.036Z" }, + { url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795, upload-time = "2025-11-30T20:23:55.556Z" }, + { url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330, upload-time = "2025-11-30T20:23:57.033Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194, upload-time = "2025-11-30T20:23:58.637Z" }, + { url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340, upload-time = "2025-11-30T20:24:00.2Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765, upload-time = "2025-11-30T20:24:01.759Z" }, + { url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834, upload-time = "2025-11-30T20:24:03.687Z" }, + { url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470, upload-time = "2025-11-30T20:24:05.232Z" }, + { url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630, upload-time = "2025-11-30T20:24:06.878Z" }, + { url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148, upload-time = "2025-11-30T20:24:08.445Z" }, + { url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030, upload-time = "2025-11-30T20:24:10.956Z" }, + { url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570, upload-time = "2025-11-30T20:24:12.735Z" }, + { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" }, + { url = "https://files.pythonhosted.org/packages/69/71/3f34339ee70521864411f8b6992e7ab13ac30d8e4e3309e07c7361767d91/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58", size = 372292, upload-time = "2025-11-30T20:24:16.537Z" }, + { url = "https://files.pythonhosted.org/packages/57/09/f183df9b8f2d66720d2ef71075c59f7e1b336bec7ee4c48f0a2b06857653/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a", size = 362128, upload-time = "2025-11-30T20:24:18.086Z" }, + { url = "https://files.pythonhosted.org/packages/7a/68/5c2594e937253457342e078f0cc1ded3dd7b2ad59afdbf2d354869110a02/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb", size = 391542, upload-time = "2025-11-30T20:24:20.092Z" }, + { url = "https://files.pythonhosted.org/packages/49/5c/31ef1afd70b4b4fbdb2800249f34c57c64beb687495b10aec0365f53dfc4/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c", size = 404004, upload-time = "2025-11-30T20:24:22.231Z" }, + { url = "https://files.pythonhosted.org/packages/e3/63/0cfbea38d05756f3440ce6534d51a491d26176ac045e2707adc99bb6e60a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3", size = 527063, upload-time = "2025-11-30T20:24:24.302Z" }, + { url = "https://files.pythonhosted.org/packages/42/e6/01e1f72a2456678b0f618fc9a1a13f882061690893c192fcad9f2926553a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5", size = 413099, upload-time = "2025-11-30T20:24:25.916Z" }, + { url = "https://files.pythonhosted.org/packages/b8/25/8df56677f209003dcbb180765520c544525e3ef21ea72279c98b9aa7c7fb/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738", size = 392177, upload-time = "2025-11-30T20:24:27.834Z" }, + { url = "https://files.pythonhosted.org/packages/4a/b4/0a771378c5f16f8115f796d1f437950158679bcd2a7c68cf251cfb00ed5b/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f", size = 406015, upload-time = "2025-11-30T20:24:29.457Z" }, + { url = "https://files.pythonhosted.org/packages/36/d8/456dbba0af75049dc6f63ff295a2f92766b9d521fa00de67a2bd6427d57a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877", size = 423736, upload-time = "2025-11-30T20:24:31.22Z" }, + { url = "https://files.pythonhosted.org/packages/13/64/b4d76f227d5c45a7e0b796c674fd81b0a6c4fbd48dc29271857d8219571c/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a", size = 573981, upload-time = "2025-11-30T20:24:32.934Z" }, + { url = "https://files.pythonhosted.org/packages/20/91/092bacadeda3edf92bf743cc96a7be133e13a39cdbfd7b5082e7ab638406/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4", size = 599782, upload-time = "2025-11-30T20:24:35.169Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b7/b95708304cd49b7b6f82fdd039f1748b66ec2b21d6a45180910802f1abf1/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e", size = 562191, upload-time = "2025-11-30T20:24:36.853Z" }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + +[[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, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "soupsieve" +version = "2.8.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/ae/2d9c981590ed9999a0d91755b47fc74f74de286b0f5cee14c9269041e6c4/soupsieve-2.8.3.tar.gz", hash = "sha256:3267f1eeea4251fb42728b6dfb746edc9acaffc4a45b27e19450b676586e8349", size = 118627, upload-time = "2026-01-20T04:27:02.457Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/2c/1462b1d0a634697ae9e55b3cecdcb64788e8b7d63f54d923fcd0bb140aed/soupsieve-2.8.3-py3-none-any.whl", hash = "sha256:ed64f2ba4eebeab06cc4962affce381647455978ffc1e36bb79a545b91f45a95", size = 37016, upload-time = "2026-01-20T04:27:01.012Z" }, +] + +[[package]] +name = "tinycss2" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "webencodings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7a/fd/7a5ee21fd08ff70d3d33a5781c255cbe779659bd03278feb98b19ee550f4/tinycss2-1.4.0.tar.gz", hash = "sha256:10c0972f6fc0fbee87c3edb76549357415e94548c1ae10ebccdea16fb404a9b7", size = 87085, upload-time = "2024-10-24T14:58:29.895Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/34/ebdc18bae6aa14fbee1a08b63c015c72b64868ff7dae68808ab500c492e2/tinycss2-1.4.0-py3-none-any.whl", hash = "sha256:3a49cf47b7675da0b15d0c6e1df8df4ebd96e9394bb905a5775adb0d884c5289", size = 26610, upload-time = "2024-10-24T14:58:28.029Z" }, +] + +[[package]] +name = "tornado" +version = "6.5.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/f1/3173dfa4a18db4a9b03e5d55325559dab51ee653763bb8745a75af491286/tornado-6.5.5.tar.gz", hash = "sha256:192b8f3ea91bd7f1f50c06955416ed76c6b72f96779b962f07f911b91e8d30e9", size = 516006, upload-time = "2026-03-10T21:31:02.067Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/8c/77f5097695f4dd8255ecbd08b2a1ed8ba8b953d337804dd7080f199e12bf/tornado-6.5.5-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:487dc9cc380e29f58c7ab88f9e27cdeef04b2140862e5076a66fb6bb68bb1bfa", size = 445983, upload-time = "2026-03-10T21:30:44.28Z" }, + { url = "https://files.pythonhosted.org/packages/ab/5e/7625b76cd10f98f1516c36ce0346de62061156352353ef2da44e5c21523c/tornado-6.5.5-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:65a7f1d46d4bb41df1ac99f5fcb685fb25c7e61613742d5108b010975a9a6521", size = 444246, upload-time = "2026-03-10T21:30:46.571Z" }, + { url = "https://files.pythonhosted.org/packages/b2/04/7b5705d5b3c0fab088f434f9c83edac1573830ca49ccf29fb83bf7178eec/tornado-6.5.5-cp39-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e74c92e8e65086b338fd56333fb9a68b9f6f2fe7ad532645a290a464bcf46be5", size = 447229, upload-time = "2026-03-10T21:30:48.273Z" }, + { url = "https://files.pythonhosted.org/packages/34/01/74e034a30ef59afb4097ef8659515e96a39d910b712a89af76f5e4e1f93c/tornado-6.5.5-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:435319e9e340276428bbdb4e7fa732c2d399386d1de5686cb331ec8eee754f07", size = 448192, upload-time = "2026-03-10T21:30:51.22Z" }, + { url = "https://files.pythonhosted.org/packages/be/00/fe9e02c5a96429fce1a1d15a517f5d8444f9c412e0bb9eadfbe3b0fc55bf/tornado-6.5.5-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:3f54aa540bdbfee7b9eb268ead60e7d199de5021facd276819c193c0fb28ea4e", size = 448039, upload-time = "2026-03-10T21:30:53.52Z" }, + { url = "https://files.pythonhosted.org/packages/82/9e/656ee4cec0398b1d18d0f1eb6372c41c6b889722641d84948351ae19556d/tornado-6.5.5-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:36abed1754faeb80fbd6e64db2758091e1320f6bba74a4cf8c09cd18ccce8aca", size = 447445, upload-time = "2026-03-10T21:30:55.541Z" }, + { url = "https://files.pythonhosted.org/packages/5a/76/4921c00511f88af86a33de770d64141170f1cfd9c00311aea689949e274e/tornado-6.5.5-cp39-abi3-win32.whl", hash = "sha256:dd3eafaaeec1c7f2f8fdcd5f964e8907ad788fe8a5a32c4426fbbdda621223b7", size = 448582, upload-time = "2026-03-10T21:30:57.142Z" }, + { url = "https://files.pythonhosted.org/packages/2c/23/f6c6112a04d28eed765e374435fb1a9198f73e1ec4b4024184f21faeb1ad/tornado-6.5.5-cp39-abi3-win_amd64.whl", hash = "sha256:6443a794ba961a9f619b1ae926a2e900ac20c34483eea67be4ed8f1e58d3ef7b", size = 448990, upload-time = "2026-03-10T21:30:58.857Z" }, + { url = "https://files.pythonhosted.org/packages/b7/c8/876602cbc96469911f0939f703453c1157b0c826ecb05bdd32e023397d4e/tornado-6.5.5-cp39-abi3-win_arm64.whl", hash = "sha256:2c9a876e094109333f888539ddb2de4361743e5d21eece20688e3e351e4990a6", size = 448016, upload-time = "2026-03-10T21:31:00.43Z" }, +] + +[[package]] +name = "traitlets" +version = "5.14.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/eb/79/72064e6a701c2183016abbbfedaba506d81e30e232a68c9f0d6f6fcd1574/traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7", size = 161621, upload-time = "2024-04-19T11:11:49.746Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359, upload-time = "2024-04-19T11:11:46.763Z" }, +] + +[[package]] +name = "typer" +version = "0.24.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/b8/9ebb531b6c2d377af08ac6746a5df3425b21853a5d2260876919b58a2a4a/typer-0.24.2.tar.gz", hash = "sha256:ec070dcfca1408e85ee203c6365001e818c3b7fffe686fd07ff2d68095ca0480", size = 119849, upload-time = "2026-04-22T17:45:34.413Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/d1/9484b497e0a0410b901c12b8251c3e746e1e863f7d28419ffe06f7892fda/typer-0.24.2-py3-none-any.whl", hash = "sha256:b618bc3d721f9a8d30f3e05565be26416d06e9bcc29d49bc491dc26aba674fa8", size = 55977, upload-time = "2026-04-22T17:45:33.055Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "webencodings" +version = "0.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/02/ae6ceac1baeda530866a85075641cec12989bd8d31af6d5ab4a3e8c92f47/webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923", size = 9721, upload-time = "2017-04-05T20:21:34.189Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", size = 11774, upload-time = "2017-04-05T20:21:32.581Z" }, +] + +[[package]] +name = "websockets" +version = "16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", size = 179346, upload-time = "2026-01-10T09:23:47.181Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/db/de907251b4ff46ae804ad0409809504153b3f30984daf82a1d84a9875830/websockets-16.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:31a52addea25187bde0797a97d6fc3d2f92b6f72a9370792d65a6e84615ac8a8", size = 177340, upload-time = "2026-01-10T09:22:34.539Z" }, + { url = "https://files.pythonhosted.org/packages/f3/fa/abe89019d8d8815c8781e90d697dec52523fb8ebe308bf11664e8de1877e/websockets-16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:417b28978cdccab24f46400586d128366313e8a96312e4b9362a4af504f3bbad", size = 175022, upload-time = "2026-01-10T09:22:36.332Z" }, + { url = "https://files.pythonhosted.org/packages/58/5d/88ea17ed1ded2079358b40d31d48abe90a73c9e5819dbcde1606e991e2ad/websockets-16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:af80d74d4edfa3cb9ed973a0a5ba2b2a549371f8a741e0800cb07becdd20f23d", size = 175319, upload-time = "2026-01-10T09:22:37.602Z" }, + { url = "https://files.pythonhosted.org/packages/d2/ae/0ee92b33087a33632f37a635e11e1d99d429d3d323329675a6022312aac2/websockets-16.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:08d7af67b64d29823fed316505a89b86705f2b7981c07848fb5e3ea3020c1abe", size = 184631, upload-time = "2026-01-10T09:22:38.789Z" }, + { url = "https://files.pythonhosted.org/packages/c8/c5/27178df583b6c5b31b29f526ba2da5e2f864ecc79c99dae630a85d68c304/websockets-16.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7be95cfb0a4dae143eaed2bcba8ac23f4892d8971311f1b06f3c6b78952ee70b", size = 185870, upload-time = "2026-01-10T09:22:39.893Z" }, + { url = "https://files.pythonhosted.org/packages/87/05/536652aa84ddc1c018dbb7e2c4cbcd0db884580bf8e95aece7593fde526f/websockets-16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d6297ce39ce5c2e6feb13c1a996a2ded3b6832155fcfc920265c76f24c7cceb5", size = 185361, upload-time = "2026-01-10T09:22:41.016Z" }, + { url = "https://files.pythonhosted.org/packages/6d/e2/d5332c90da12b1e01f06fb1b85c50cfc489783076547415bf9f0a659ec19/websockets-16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1c1b30e4f497b0b354057f3467f56244c603a79c0d1dafce1d16c283c25f6e64", size = 184615, upload-time = "2026-01-10T09:22:42.442Z" }, + { url = "https://files.pythonhosted.org/packages/77/fb/d3f9576691cae9253b51555f841bc6600bf0a983a461c79500ace5a5b364/websockets-16.0-cp311-cp311-win32.whl", hash = "sha256:5f451484aeb5cafee1ccf789b1b66f535409d038c56966d6101740c1614b86c6", size = 178246, upload-time = "2026-01-10T09:22:43.654Z" }, + { url = "https://files.pythonhosted.org/packages/54/67/eaff76b3dbaf18dcddabc3b8c1dba50b483761cccff67793897945b37408/websockets-16.0-cp311-cp311-win_amd64.whl", hash = "sha256:8d7f0659570eefb578dacde98e24fb60af35350193e4f56e11190787bee77dac", size = 178684, upload-time = "2026-01-10T09:22:44.941Z" }, + { url = "https://files.pythonhosted.org/packages/84/7b/bac442e6b96c9d25092695578dda82403c77936104b5682307bd4deb1ad4/websockets-16.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:71c989cbf3254fbd5e84d3bff31e4da39c43f884e64f2551d14bb3c186230f00", size = 177365, upload-time = "2026-01-10T09:22:46.787Z" }, + { url = "https://files.pythonhosted.org/packages/b0/fe/136ccece61bd690d9c1f715baaeefd953bb2360134de73519d5df19d29ca/websockets-16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8b6e209ffee39ff1b6d0fa7bfef6de950c60dfb91b8fcead17da4ee539121a79", size = 175038, upload-time = "2026-01-10T09:22:47.999Z" }, + { url = "https://files.pythonhosted.org/packages/40/1e/9771421ac2286eaab95b8575b0cb701ae3663abf8b5e1f64f1fd90d0a673/websockets-16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86890e837d61574c92a97496d590968b23c2ef0aeb8a9bc9421d174cd378ae39", size = 175328, upload-time = "2026-01-10T09:22:49.809Z" }, + { url = "https://files.pythonhosted.org/packages/18/29/71729b4671f21e1eaa5d6573031ab810ad2936c8175f03f97f3ff164c802/websockets-16.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9b5aca38b67492ef518a8ab76851862488a478602229112c4b0d58d63a7a4d5c", size = 184915, upload-time = "2026-01-10T09:22:51.071Z" }, + { url = "https://files.pythonhosted.org/packages/97/bb/21c36b7dbbafc85d2d480cd65df02a1dc93bf76d97147605a8e27ff9409d/websockets-16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e0334872c0a37b606418ac52f6ab9cfd17317ac26365f7f65e203e2d0d0d359f", size = 186152, upload-time = "2026-01-10T09:22:52.224Z" }, + { url = "https://files.pythonhosted.org/packages/4a/34/9bf8df0c0cf88fa7bfe36678dc7b02970c9a7d5e065a3099292db87b1be2/websockets-16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a0b31e0b424cc6b5a04b8838bbaec1688834b2383256688cf47eb97412531da1", size = 185583, upload-time = "2026-01-10T09:22:53.443Z" }, + { url = "https://files.pythonhosted.org/packages/47/88/4dd516068e1a3d6ab3c7c183288404cd424a9a02d585efbac226cb61ff2d/websockets-16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:485c49116d0af10ac698623c513c1cc01c9446c058a4e61e3bf6c19dff7335a2", size = 184880, upload-time = "2026-01-10T09:22:55.033Z" }, + { url = "https://files.pythonhosted.org/packages/91/d6/7d4553ad4bf1c0421e1ebd4b18de5d9098383b5caa1d937b63df8d04b565/websockets-16.0-cp312-cp312-win32.whl", hash = "sha256:eaded469f5e5b7294e2bdca0ab06becb6756ea86894a47806456089298813c89", size = 178261, upload-time = "2026-01-10T09:22:56.251Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f0/f3a17365441ed1c27f850a80b2bc680a0fa9505d733fe152fdf5e98c1c0b/websockets-16.0-cp312-cp312-win_amd64.whl", hash = "sha256:5569417dc80977fc8c2d43a86f78e0a5a22fee17565d78621b6bb264a115d4ea", size = 178693, upload-time = "2026-01-10T09:22:57.478Z" }, + { url = "https://files.pythonhosted.org/packages/cc/9c/baa8456050d1c1b08dd0ec7346026668cbc6f145ab4e314d707bb845bf0d/websockets-16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:878b336ac47938b474c8f982ac2f7266a540adc3fa4ad74ae96fea9823a02cc9", size = 177364, upload-time = "2026-01-10T09:22:59.333Z" }, + { url = "https://files.pythonhosted.org/packages/7e/0c/8811fc53e9bcff68fe7de2bcbe75116a8d959ac699a3200f4847a8925210/websockets-16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:52a0fec0e6c8d9a784c2c78276a48a2bdf099e4ccc2a4cad53b27718dbfd0230", size = 175039, upload-time = "2026-01-10T09:23:01.171Z" }, + { url = "https://files.pythonhosted.org/packages/aa/82/39a5f910cb99ec0b59e482971238c845af9220d3ab9fa76dd9162cda9d62/websockets-16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6578ed5b6981005df1860a56e3617f14a6c307e6a71b4fff8c48fdc50f3ed2c", size = 175323, upload-time = "2026-01-10T09:23:02.341Z" }, + { url = "https://files.pythonhosted.org/packages/bd/28/0a25ee5342eb5d5f297d992a77e56892ecb65e7854c7898fb7d35e9b33bd/websockets-16.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95724e638f0f9c350bb1c2b0a7ad0e83d9cc0c9259f3ea94e40d7b02a2179ae5", size = 184975, upload-time = "2026-01-10T09:23:03.756Z" }, + { url = "https://files.pythonhosted.org/packages/f9/66/27ea52741752f5107c2e41fda05e8395a682a1e11c4e592a809a90c6a506/websockets-16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0204dc62a89dc9d50d682412c10b3542d748260d743500a85c13cd1ee4bde82", size = 186203, upload-time = "2026-01-10T09:23:05.01Z" }, + { url = "https://files.pythonhosted.org/packages/37/e5/8e32857371406a757816a2b471939d51c463509be73fa538216ea52b792a/websockets-16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52ac480f44d32970d66763115edea932f1c5b1312de36df06d6b219f6741eed8", size = 185653, upload-time = "2026-01-10T09:23:06.301Z" }, + { url = "https://files.pythonhosted.org/packages/9b/67/f926bac29882894669368dc73f4da900fcdf47955d0a0185d60103df5737/websockets-16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6e5a82b677f8f6f59e8dfc34ec06ca6b5b48bc4fcda346acd093694cc2c24d8f", size = 184920, upload-time = "2026-01-10T09:23:07.492Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a1/3d6ccdcd125b0a42a311bcd15a7f705d688f73b2a22d8cf1c0875d35d34a/websockets-16.0-cp313-cp313-win32.whl", hash = "sha256:abf050a199613f64c886ea10f38b47770a65154dc37181bfaff70c160f45315a", size = 178255, upload-time = "2026-01-10T09:23:09.245Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ae/90366304d7c2ce80f9b826096a9e9048b4bb760e44d3b873bb272cba696b/websockets-16.0-cp313-cp313-win_amd64.whl", hash = "sha256:3425ac5cf448801335d6fdc7ae1eb22072055417a96cc6b31b3861f455fbc156", size = 178689, upload-time = "2026-01-10T09:23:10.483Z" }, + { url = "https://files.pythonhosted.org/packages/f3/1d/e88022630271f5bd349ed82417136281931e558d628dd52c4d8621b4a0b2/websockets-16.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0", size = 177406, upload-time = "2026-01-10T09:23:12.178Z" }, + { url = "https://files.pythonhosted.org/packages/f2/78/e63be1bf0724eeb4616efb1ae1c9044f7c3953b7957799abb5915bffd38e/websockets-16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:daa3b6ff70a9241cf6c7fc9e949d41232d9d7d26fd3522b1ad2b4d62487e9904", size = 175085, upload-time = "2026-01-10T09:23:13.511Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f4/d3c9220d818ee955ae390cf319a7c7a467beceb24f05ee7aaaa2414345ba/websockets-16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4", size = 175328, upload-time = "2026-01-10T09:23:14.727Z" }, + { url = "https://files.pythonhosted.org/packages/63/bc/d3e208028de777087e6fb2b122051a6ff7bbcca0d6df9d9c2bf1dd869ae9/websockets-16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:781caf5e8eee67f663126490c2f96f40906594cb86b408a703630f95550a8c3e", size = 185044, upload-time = "2026-01-10T09:23:15.939Z" }, + { url = "https://files.pythonhosted.org/packages/ad/6e/9a0927ac24bd33a0a9af834d89e0abc7cfd8e13bed17a86407a66773cc0e/websockets-16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caab51a72c51973ca21fa8a18bd8165e1a0183f1ac7066a182ff27107b71e1a4", size = 186279, upload-time = "2026-01-10T09:23:17.148Z" }, + { url = "https://files.pythonhosted.org/packages/b9/ca/bf1c68440d7a868180e11be653c85959502efd3a709323230314fda6e0b3/websockets-16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1", size = 185711, upload-time = "2026-01-10T09:23:18.372Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f8/fdc34643a989561f217bb477cbc47a3a07212cbda91c0e4389c43c296ebf/websockets-16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5e18a238a2b2249c9a9235466b90e96ae4795672598a58772dd806edc7ac6d3", size = 184982, upload-time = "2026-01-10T09:23:19.652Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d1/574fa27e233764dbac9c52730d63fcf2823b16f0856b3329fc6268d6ae4f/websockets-16.0-cp314-cp314-win32.whl", hash = "sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8", size = 177915, upload-time = "2026-01-10T09:23:21.458Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f1/ae6b937bf3126b5134ce1f482365fde31a357c784ac51852978768b5eff4/websockets-16.0-cp314-cp314-win_amd64.whl", hash = "sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d", size = 178381, upload-time = "2026-01-10T09:23:22.715Z" }, + { url = "https://files.pythonhosted.org/packages/06/9b/f791d1db48403e1f0a27577a6beb37afae94254a8c6f08be4a23e4930bc0/websockets-16.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a35539cacc3febb22b8f4d4a99cc79b104226a756aa7400adc722e83b0d03244", size = 177737, upload-time = "2026-01-10T09:23:24.523Z" }, + { url = "https://files.pythonhosted.org/packages/bd/40/53ad02341fa33b3ce489023f635367a4ac98b73570102ad2cdd770dacc9a/websockets-16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b784ca5de850f4ce93ec85d3269d24d4c82f22b7212023c974c401d4980ebc5e", size = 175268, upload-time = "2026-01-10T09:23:25.781Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/6158d4e459b984f949dcbbb0c5d270154c7618e11c01029b9bbd1bb4c4f9/websockets-16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:569d01a4e7fba956c5ae4fc988f0d4e187900f5497ce46339c996dbf24f17641", size = 175486, upload-time = "2026-01-10T09:23:27.033Z" }, + { url = "https://files.pythonhosted.org/packages/e5/2d/7583b30208b639c8090206f95073646c2c9ffd66f44df967981a64f849ad/websockets-16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50f23cdd8343b984957e4077839841146f67a3d31ab0d00e6b824e74c5b2f6e8", size = 185331, upload-time = "2026-01-10T09:23:28.259Z" }, + { url = "https://files.pythonhosted.org/packages/45/b0/cce3784eb519b7b5ad680d14b9673a31ab8dcb7aad8b64d81709d2430aa8/websockets-16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:152284a83a00c59b759697b7f9e9cddf4e3c7861dd0d964b472b70f78f89e80e", size = 186501, upload-time = "2026-01-10T09:23:29.449Z" }, + { url = "https://files.pythonhosted.org/packages/19/60/b8ebe4c7e89fb5f6cdf080623c9d92789a53636950f7abacfc33fe2b3135/websockets-16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944", size = 186062, upload-time = "2026-01-10T09:23:31.368Z" }, + { url = "https://files.pythonhosted.org/packages/88/a8/a080593f89b0138b6cba1b28f8df5673b5506f72879322288b031337c0b8/websockets-16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206", size = 185356, upload-time = "2026-01-10T09:23:32.627Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b6/b9afed2afadddaf5ebb2afa801abf4b0868f42f8539bfe4b071b5266c9fe/websockets-16.0-cp314-cp314t-win32.whl", hash = "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6", size = 178085, upload-time = "2026-01-10T09:23:33.816Z" }, + { url = "https://files.pythonhosted.org/packages/9f/3e/28135a24e384493fa804216b79a6a6759a38cc4ff59118787b9fb693df93/websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd", size = 178531, upload-time = "2026-01-10T09:23:35.016Z" }, + { url = "https://files.pythonhosted.org/packages/72/07/c98a68571dcf256e74f1f816b8cc5eae6eb2d3d5cfa44d37f801619d9166/websockets-16.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:349f83cd6c9a415428ee1005cadb5c2c56f4389bc06a9af16103c3bc3dcc8b7d", size = 174947, upload-time = "2026-01-10T09:23:36.166Z" }, + { url = "https://files.pythonhosted.org/packages/7e/52/93e166a81e0305b33fe416338be92ae863563fe7bce446b0f687b9df5aea/websockets-16.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:4a1aba3340a8dca8db6eb5a7986157f52eb9e436b74813764241981ca4888f03", size = 175260, upload-time = "2026-01-10T09:23:37.409Z" }, + { url = "https://files.pythonhosted.org/packages/56/0c/2dbf513bafd24889d33de2ff0368190a0e69f37bcfa19009ef819fe4d507/websockets-16.0-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f4a32d1bd841d4bcbffdcb3d2ce50c09c3909fbead375ab28d0181af89fd04da", size = 176071, upload-time = "2026-01-10T09:23:39.158Z" }, + { url = "https://files.pythonhosted.org/packages/a5/8f/aea9c71cc92bf9b6cc0f7f70df8f0b420636b6c96ef4feee1e16f80f75dd/websockets-16.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0298d07ee155e2e9fda5be8a9042200dd2e3bb0b8a38482156576f863a9d457c", size = 176968, upload-time = "2026-01-10T09:23:41.031Z" }, + { url = "https://files.pythonhosted.org/packages/9a/3f/f70e03f40ffc9a30d817eef7da1be72ee4956ba8d7255c399a01b135902a/websockets-16.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:a653aea902e0324b52f1613332ddf50b00c06fdaf7e92624fbf8c77c78fa5767", size = 178735, upload-time = "2026-01-10T09:23:42.259Z" }, + { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" }, +] + +[[package]] +name = "xxhash" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/02/84/30869e01909fb37a6cc7e18688ee8bf1e42d57e7e0777636bd47524c43c7/xxhash-3.6.0.tar.gz", hash = "sha256:f0162a78b13a0d7617b2845b90c763339d1f1d82bb04a4b07f4ab535cc5e05d6", size = 85160, upload-time = "2025-10-02T14:37:08.097Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/d4/cc2f0400e9154df4b9964249da78ebd72f318e35ccc425e9f403c392f22a/xxhash-3.6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b47bbd8cf2d72797f3c2772eaaac0ded3d3af26481a26d7d7d41dc2d3c46b04a", size = 32844, upload-time = "2025-10-02T14:34:14.037Z" }, + { url = "https://files.pythonhosted.org/packages/5e/ec/1cc11cd13e26ea8bc3cb4af4eaadd8d46d5014aebb67be3f71fb0b68802a/xxhash-3.6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2b6821e94346f96db75abaa6e255706fb06ebd530899ed76d32cd99f20dc52fa", size = 30809, upload-time = "2025-10-02T14:34:15.484Z" }, + { url = "https://files.pythonhosted.org/packages/04/5f/19fe357ea348d98ca22f456f75a30ac0916b51c753e1f8b2e0e6fb884cce/xxhash-3.6.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d0a9751f71a1a65ce3584e9cae4467651c7e70c9d31017fa57574583a4540248", size = 194665, upload-time = "2025-10-02T14:34:16.541Z" }, + { url = "https://files.pythonhosted.org/packages/90/3b/d1f1a8f5442a5fd8beedae110c5af7604dc37349a8e16519c13c19a9a2de/xxhash-3.6.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b29ee68625ab37b04c0b40c3fafdf24d2f75ccd778333cfb698f65f6c463f62", size = 213550, upload-time = "2025-10-02T14:34:17.878Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ef/3a9b05eb527457d5db13a135a2ae1a26c80fecd624d20f3e8dcc4cb170f3/xxhash-3.6.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6812c25fe0d6c36a46ccb002f40f27ac903bf18af9f6dd8f9669cb4d176ab18f", size = 212384, upload-time = "2025-10-02T14:34:19.182Z" }, + { url = "https://files.pythonhosted.org/packages/0f/18/ccc194ee698c6c623acbf0f8c2969811a8a4b6185af5e824cd27b9e4fd3e/xxhash-3.6.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4ccbff013972390b51a18ef1255ef5ac125c92dc9143b2d1909f59abc765540e", size = 445749, upload-time = "2025-10-02T14:34:20.659Z" }, + { url = "https://files.pythonhosted.org/packages/a5/86/cf2c0321dc3940a7aa73076f4fd677a0fb3e405cb297ead7d864fd90847e/xxhash-3.6.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:297b7fbf86c82c550e12e8fb71968b3f033d27b874276ba3624ea868c11165a8", size = 193880, upload-time = "2025-10-02T14:34:22.431Z" }, + { url = "https://files.pythonhosted.org/packages/82/fb/96213c8560e6f948a1ecc9a7613f8032b19ee45f747f4fca4eb31bb6d6ed/xxhash-3.6.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dea26ae1eb293db089798d3973a5fc928a18fdd97cc8801226fae705b02b14b0", size = 210912, upload-time = "2025-10-02T14:34:23.937Z" }, + { url = "https://files.pythonhosted.org/packages/40/aa/4395e669b0606a096d6788f40dbdf2b819d6773aa290c19e6e83cbfc312f/xxhash-3.6.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7a0b169aafb98f4284f73635a8e93f0735f9cbde17bd5ec332480484241aaa77", size = 198654, upload-time = "2025-10-02T14:34:25.644Z" }, + { url = "https://files.pythonhosted.org/packages/67/74/b044fcd6b3d89e9b1b665924d85d3f400636c23590226feb1eb09e1176ce/xxhash-3.6.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:08d45aef063a4531b785cd72de4887766d01dc8f362a515693df349fdb825e0c", size = 210867, upload-time = "2025-10-02T14:34:27.203Z" }, + { url = "https://files.pythonhosted.org/packages/bc/fd/3ce73bf753b08cb19daee1eb14aa0d7fe331f8da9c02dd95316ddfe5275e/xxhash-3.6.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:929142361a48ee07f09121fe9e96a84950e8d4df3bb298ca5d88061969f34d7b", size = 414012, upload-time = "2025-10-02T14:34:28.409Z" }, + { url = "https://files.pythonhosted.org/packages/ba/b3/5a4241309217c5c876f156b10778f3ab3af7ba7e3259e6d5f5c7d0129eb2/xxhash-3.6.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:51312c768403d8540487dbbfb557454cfc55589bbde6424456951f7fcd4facb3", size = 191409, upload-time = "2025-10-02T14:34:29.696Z" }, + { url = "https://files.pythonhosted.org/packages/c0/01/99bfbc15fb9abb9a72b088c1d95219fc4782b7d01fc835bd5744d66dd0b8/xxhash-3.6.0-cp311-cp311-win32.whl", hash = "sha256:d1927a69feddc24c987b337ce81ac15c4720955b667fe9b588e02254b80446fd", size = 30574, upload-time = "2025-10-02T14:34:31.028Z" }, + { url = "https://files.pythonhosted.org/packages/65/79/9d24d7f53819fe301b231044ea362ce64e86c74f6e8c8e51320de248b3e5/xxhash-3.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:26734cdc2d4ffe449b41d186bbeac416f704a482ed835d375a5c0cb02bc63fef", size = 31481, upload-time = "2025-10-02T14:34:32.062Z" }, + { url = "https://files.pythonhosted.org/packages/30/4e/15cd0e3e8772071344eab2961ce83f6e485111fed8beb491a3f1ce100270/xxhash-3.6.0-cp311-cp311-win_arm64.whl", hash = "sha256:d72f67ef8bf36e05f5b6c65e8524f265bd61071471cd4cf1d36743ebeeeb06b7", size = 27861, upload-time = "2025-10-02T14:34:33.555Z" }, + { url = "https://files.pythonhosted.org/packages/9a/07/d9412f3d7d462347e4511181dea65e47e0d0e16e26fbee2ea86a2aefb657/xxhash-3.6.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:01362c4331775398e7bb34e3ab403bc9ee9f7c497bc7dee6272114055277dd3c", size = 32744, upload-time = "2025-10-02T14:34:34.622Z" }, + { url = "https://files.pythonhosted.org/packages/79/35/0429ee11d035fc33abe32dca1b2b69e8c18d236547b9a9b72c1929189b9a/xxhash-3.6.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b7b2df81a23f8cb99656378e72501b2cb41b1827c0f5a86f87d6b06b69f9f204", size = 30816, upload-time = "2025-10-02T14:34:36.043Z" }, + { url = "https://files.pythonhosted.org/packages/b7/f2/57eb99aa0f7d98624c0932c5b9a170e1806406cdbcdb510546634a1359e0/xxhash-3.6.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:dc94790144e66b14f67b10ac8ed75b39ca47536bf8800eb7c24b50271ea0c490", size = 194035, upload-time = "2025-10-02T14:34:37.354Z" }, + { url = "https://files.pythonhosted.org/packages/4c/ed/6224ba353690d73af7a3f1c7cdb1fc1b002e38f783cb991ae338e1eb3d79/xxhash-3.6.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:93f107c673bccf0d592cdba077dedaf52fe7f42dcd7676eba1f6d6f0c3efffd2", size = 212914, upload-time = "2025-10-02T14:34:38.6Z" }, + { url = "https://files.pythonhosted.org/packages/38/86/fb6b6130d8dd6b8942cc17ab4d90e223653a89aa32ad2776f8af7064ed13/xxhash-3.6.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2aa5ee3444c25b69813663c9f8067dcfaa2e126dc55e8dddf40f4d1c25d7effa", size = 212163, upload-time = "2025-10-02T14:34:39.872Z" }, + { url = "https://files.pythonhosted.org/packages/ee/dc/e84875682b0593e884ad73b2d40767b5790d417bde603cceb6878901d647/xxhash-3.6.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7f99123f0e1194fa59cc69ad46dbae2e07becec5df50a0509a808f90a0f03f0", size = 445411, upload-time = "2025-10-02T14:34:41.569Z" }, + { url = "https://files.pythonhosted.org/packages/11/4f/426f91b96701ec2f37bb2b8cec664eff4f658a11f3fa9d94f0a887ea6d2b/xxhash-3.6.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:49e03e6fe2cac4a1bc64952dd250cf0dbc5ef4ebb7b8d96bce82e2de163c82a2", size = 193883, upload-time = "2025-10-02T14:34:43.249Z" }, + { url = "https://files.pythonhosted.org/packages/53/5a/ddbb83eee8e28b778eacfc5a85c969673e4023cdeedcfcef61f36731610b/xxhash-3.6.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bd17fede52a17a4f9a7bc4472a5867cb0b160deeb431795c0e4abe158bc784e9", size = 210392, upload-time = "2025-10-02T14:34:45.042Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c2/ff69efd07c8c074ccdf0a4f36fcdd3d27363665bcdf4ba399abebe643465/xxhash-3.6.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:6fb5f5476bef678f69db04f2bd1efbed3030d2aba305b0fc1773645f187d6a4e", size = 197898, upload-time = "2025-10-02T14:34:46.302Z" }, + { url = "https://files.pythonhosted.org/packages/58/ca/faa05ac19b3b622c7c9317ac3e23954187516298a091eb02c976d0d3dd45/xxhash-3.6.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:843b52f6d88071f87eba1631b684fcb4b2068cd2180a0224122fe4ef011a9374", size = 210655, upload-time = "2025-10-02T14:34:47.571Z" }, + { url = "https://files.pythonhosted.org/packages/d4/7a/06aa7482345480cc0cb597f5c875b11a82c3953f534394f620b0be2f700c/xxhash-3.6.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7d14a6cfaf03b1b6f5f9790f76880601ccc7896aff7ab9cd8978a939c1eb7e0d", size = 414001, upload-time = "2025-10-02T14:34:49.273Z" }, + { url = "https://files.pythonhosted.org/packages/23/07/63ffb386cd47029aa2916b3d2f454e6cc5b9f5c5ada3790377d5430084e7/xxhash-3.6.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:418daf3db71e1413cfe211c2f9a528456936645c17f46b5204705581a45390ae", size = 191431, upload-time = "2025-10-02T14:34:50.798Z" }, + { url = "https://files.pythonhosted.org/packages/0f/93/14fde614cadb4ddf5e7cebf8918b7e8fac5ae7861c1875964f17e678205c/xxhash-3.6.0-cp312-cp312-win32.whl", hash = "sha256:50fc255f39428a27299c20e280d6193d8b63b8ef8028995323bf834a026b4fbb", size = 30617, upload-time = "2025-10-02T14:34:51.954Z" }, + { url = "https://files.pythonhosted.org/packages/13/5d/0d125536cbe7565a83d06e43783389ecae0c0f2ed037b48ede185de477c0/xxhash-3.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:c0f2ab8c715630565ab8991b536ecded9416d615538be8ecddce43ccf26cbc7c", size = 31534, upload-time = "2025-10-02T14:34:53.276Z" }, + { url = "https://files.pythonhosted.org/packages/54/85/6ec269b0952ec7e36ba019125982cf11d91256a778c7c3f98a4c5043d283/xxhash-3.6.0-cp312-cp312-win_arm64.whl", hash = "sha256:eae5c13f3bc455a3bbb68bdc513912dc7356de7e2280363ea235f71f54064829", size = 27876, upload-time = "2025-10-02T14:34:54.371Z" }, + { url = "https://files.pythonhosted.org/packages/33/76/35d05267ac82f53ae9b0e554da7c5e281ee61f3cad44c743f0fcd354f211/xxhash-3.6.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:599e64ba7f67472481ceb6ee80fa3bd828fd61ba59fb11475572cc5ee52b89ec", size = 32738, upload-time = "2025-10-02T14:34:55.839Z" }, + { url = "https://files.pythonhosted.org/packages/31/a8/3fbce1cd96534a95e35d5120637bf29b0d7f5d8fa2f6374e31b4156dd419/xxhash-3.6.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7d8b8aaa30fca4f16f0c84a5c8d7ddee0e25250ec2796c973775373257dde8f1", size = 30821, upload-time = "2025-10-02T14:34:57.219Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ea/d387530ca7ecfa183cb358027f1833297c6ac6098223fd14f9782cd0015c/xxhash-3.6.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d597acf8506d6e7101a4a44a5e428977a51c0fadbbfd3c39650cca9253f6e5a6", size = 194127, upload-time = "2025-10-02T14:34:59.21Z" }, + { url = "https://files.pythonhosted.org/packages/ba/0c/71435dcb99874b09a43b8d7c54071e600a7481e42b3e3ce1eb5226a5711a/xxhash-3.6.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:858dc935963a33bc33490128edc1c12b0c14d9c7ebaa4e387a7869ecc4f3e263", size = 212975, upload-time = "2025-10-02T14:35:00.816Z" }, + { url = "https://files.pythonhosted.org/packages/84/7a/c2b3d071e4bb4a90b7057228a99b10d51744878f4a8a6dd643c8bd897620/xxhash-3.6.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba284920194615cb8edf73bf52236ce2e1664ccd4a38fdb543506413529cc546", size = 212241, upload-time = "2025-10-02T14:35:02.207Z" }, + { url = "https://files.pythonhosted.org/packages/81/5f/640b6eac0128e215f177df99eadcd0f1b7c42c274ab6a394a05059694c5a/xxhash-3.6.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4b54219177f6c6674d5378bd862c6aedf64725f70dd29c472eaae154df1a2e89", size = 445471, upload-time = "2025-10-02T14:35:03.61Z" }, + { url = "https://files.pythonhosted.org/packages/5e/1e/3c3d3ef071b051cc3abbe3721ffb8365033a172613c04af2da89d5548a87/xxhash-3.6.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:42c36dd7dbad2f5238950c377fcbf6811b1cdb1c444fab447960030cea60504d", size = 193936, upload-time = "2025-10-02T14:35:05.013Z" }, + { url = "https://files.pythonhosted.org/packages/2c/bd/4a5f68381939219abfe1c22a9e3a5854a4f6f6f3c4983a87d255f21f2e5d/xxhash-3.6.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f22927652cba98c44639ffdc7aaf35828dccf679b10b31c4ad72a5b530a18eb7", size = 210440, upload-time = "2025-10-02T14:35:06.239Z" }, + { url = "https://files.pythonhosted.org/packages/eb/37/b80fe3d5cfb9faff01a02121a0f4d565eb7237e9e5fc66e73017e74dcd36/xxhash-3.6.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b45fad44d9c5c119e9c6fbf2e1c656a46dc68e280275007bbfd3d572b21426db", size = 197990, upload-time = "2025-10-02T14:35:07.735Z" }, + { url = "https://files.pythonhosted.org/packages/d7/fd/2c0a00c97b9e18f72e1f240ad4e8f8a90fd9d408289ba9c7c495ed7dc05c/xxhash-3.6.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:6f2580ffab1a8b68ef2b901cde7e55fa8da5e4be0977c68f78fc80f3c143de42", size = 210689, upload-time = "2025-10-02T14:35:09.438Z" }, + { url = "https://files.pythonhosted.org/packages/93/86/5dd8076a926b9a95db3206aba20d89a7fc14dd5aac16e5c4de4b56033140/xxhash-3.6.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:40c391dd3cd041ebc3ffe6f2c862f402e306eb571422e0aa918d8070ba31da11", size = 414068, upload-time = "2025-10-02T14:35:11.162Z" }, + { url = "https://files.pythonhosted.org/packages/af/3c/0bb129170ee8f3650f08e993baee550a09593462a5cddd8e44d0011102b1/xxhash-3.6.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f205badabde7aafd1a31e8ca2a3e5a763107a71c397c4481d6a804eb5063d8bd", size = 191495, upload-time = "2025-10-02T14:35:12.971Z" }, + { url = "https://files.pythonhosted.org/packages/e9/3a/6797e0114c21d1725e2577508e24006fd7ff1d8c0c502d3b52e45c1771d8/xxhash-3.6.0-cp313-cp313-win32.whl", hash = "sha256:2577b276e060b73b73a53042ea5bd5203d3e6347ce0d09f98500f418a9fcf799", size = 30620, upload-time = "2025-10-02T14:35:14.129Z" }, + { url = "https://files.pythonhosted.org/packages/86/15/9bc32671e9a38b413a76d24722a2bf8784a132c043063a8f5152d390b0f9/xxhash-3.6.0-cp313-cp313-win_amd64.whl", hash = "sha256:757320d45d2fbcce8f30c42a6b2f47862967aea7bf458b9625b4bbe7ee390392", size = 31542, upload-time = "2025-10-02T14:35:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/39/c5/cc01e4f6188656e56112d6a8e0dfe298a16934b8c47a247236549a3f7695/xxhash-3.6.0-cp313-cp313-win_arm64.whl", hash = "sha256:457b8f85dec5825eed7b69c11ae86834a018b8e3df5e77783c999663da2f96d6", size = 27880, upload-time = "2025-10-02T14:35:16.315Z" }, + { url = "https://files.pythonhosted.org/packages/f3/30/25e5321c8732759e930c555176d37e24ab84365482d257c3b16362235212/xxhash-3.6.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a42e633d75cdad6d625434e3468126c73f13f7584545a9cf34e883aa1710e702", size = 32956, upload-time = "2025-10-02T14:35:17.413Z" }, + { url = "https://files.pythonhosted.org/packages/9f/3c/0573299560d7d9f8ab1838f1efc021a280b5ae5ae2e849034ef3dee18810/xxhash-3.6.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:568a6d743219e717b07b4e03b0a828ce593833e498c3b64752e0f5df6bfe84db", size = 31072, upload-time = "2025-10-02T14:35:18.844Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1c/52d83a06e417cd9d4137722693424885cc9878249beb3a7c829e74bf7ce9/xxhash-3.6.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bec91b562d8012dae276af8025a55811b875baace6af510412a5e58e3121bc54", size = 196409, upload-time = "2025-10-02T14:35:20.31Z" }, + { url = "https://files.pythonhosted.org/packages/e3/8e/c6d158d12a79bbd0b878f8355432075fc82759e356ab5a111463422a239b/xxhash-3.6.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78e7f2f4c521c30ad5e786fdd6bae89d47a32672a80195467b5de0480aa97b1f", size = 215736, upload-time = "2025-10-02T14:35:21.616Z" }, + { url = "https://files.pythonhosted.org/packages/bc/68/c4c80614716345d55071a396cf03d06e34b5f4917a467faf43083c995155/xxhash-3.6.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3ed0df1b11a79856df5ffcab572cbd6b9627034c1c748c5566fa79df9048a7c5", size = 214833, upload-time = "2025-10-02T14:35:23.32Z" }, + { url = "https://files.pythonhosted.org/packages/7e/e9/ae27c8ffec8b953efa84c7c4a6c6802c263d587b9fc0d6e7cea64e08c3af/xxhash-3.6.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0e4edbfc7d420925b0dd5e792478ed393d6e75ff8fc219a6546fb446b6a417b1", size = 448348, upload-time = "2025-10-02T14:35:25.111Z" }, + { url = "https://files.pythonhosted.org/packages/d7/6b/33e21afb1b5b3f46b74b6bd1913639066af218d704cc0941404ca717fc57/xxhash-3.6.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fba27a198363a7ef87f8c0f6b171ec36b674fe9053742c58dd7e3201c1ab30ee", size = 196070, upload-time = "2025-10-02T14:35:26.586Z" }, + { url = "https://files.pythonhosted.org/packages/96/b6/fcabd337bc5fa624e7203aa0fa7d0c49eed22f72e93229431752bddc83d9/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:794fe9145fe60191c6532fa95063765529770edcdd67b3d537793e8004cabbfd", size = 212907, upload-time = "2025-10-02T14:35:28.087Z" }, + { url = "https://files.pythonhosted.org/packages/4b/d3/9ee6160e644d660fcf176c5825e61411c7f62648728f69c79ba237250143/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:6105ef7e62b5ac73a837778efc331a591d8442f8ef5c7e102376506cb4ae2729", size = 200839, upload-time = "2025-10-02T14:35:29.857Z" }, + { url = "https://files.pythonhosted.org/packages/0d/98/e8de5baa5109394baf5118f5e72ab21a86387c4f89b0e77ef3e2f6b0327b/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:f01375c0e55395b814a679b3eea205db7919ac2af213f4a6682e01220e5fe292", size = 213304, upload-time = "2025-10-02T14:35:31.222Z" }, + { url = "https://files.pythonhosted.org/packages/7b/1d/71056535dec5c3177eeb53e38e3d367dd1d16e024e63b1cee208d572a033/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:d706dca2d24d834a4661619dcacf51a75c16d65985718d6a7d73c1eeeb903ddf", size = 416930, upload-time = "2025-10-02T14:35:32.517Z" }, + { url = "https://files.pythonhosted.org/packages/dc/6c/5cbde9de2cd967c322e651c65c543700b19e7ae3e0aae8ece3469bf9683d/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5f059d9faeacd49c0215d66f4056e1326c80503f51a1532ca336a385edadd033", size = 193787, upload-time = "2025-10-02T14:35:33.827Z" }, + { url = "https://files.pythonhosted.org/packages/19/fa/0172e350361d61febcea941b0cc541d6e6c8d65d153e85f850a7b256ff8a/xxhash-3.6.0-cp313-cp313t-win32.whl", hash = "sha256:1244460adc3a9be84731d72b8e80625788e5815b68da3da8b83f78115a40a7ec", size = 30916, upload-time = "2025-10-02T14:35:35.107Z" }, + { url = "https://files.pythonhosted.org/packages/ad/e6/e8cf858a2b19d6d45820f072eff1bea413910592ff17157cabc5f1227a16/xxhash-3.6.0-cp313-cp313t-win_amd64.whl", hash = "sha256:b1e420ef35c503869c4064f4a2f2b08ad6431ab7b229a05cce39d74268bca6b8", size = 31799, upload-time = "2025-10-02T14:35:36.165Z" }, + { url = "https://files.pythonhosted.org/packages/56/15/064b197e855bfb7b343210e82490ae672f8bc7cdf3ddb02e92f64304ee8a/xxhash-3.6.0-cp313-cp313t-win_arm64.whl", hash = "sha256:ec44b73a4220623235f67a996c862049f375df3b1052d9899f40a6382c32d746", size = 28044, upload-time = "2025-10-02T14:35:37.195Z" }, + { url = "https://files.pythonhosted.org/packages/7e/5e/0138bc4484ea9b897864d59fce9be9086030825bc778b76cb5a33a906d37/xxhash-3.6.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a40a3d35b204b7cc7643cbcf8c9976d818cb47befcfac8bbefec8038ac363f3e", size = 32754, upload-time = "2025-10-02T14:35:38.245Z" }, + { url = "https://files.pythonhosted.org/packages/18/d7/5dac2eb2ec75fd771957a13e5dda560efb2176d5203f39502a5fc571f899/xxhash-3.6.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a54844be970d3fc22630b32d515e79a90d0a3ddb2644d8d7402e3c4c8da61405", size = 30846, upload-time = "2025-10-02T14:35:39.6Z" }, + { url = "https://files.pythonhosted.org/packages/fe/71/8bc5be2bb00deb5682e92e8da955ebe5fa982da13a69da5a40a4c8db12fb/xxhash-3.6.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:016e9190af8f0a4e3741343777710e3d5717427f175adfdc3e72508f59e2a7f3", size = 194343, upload-time = "2025-10-02T14:35:40.69Z" }, + { url = "https://files.pythonhosted.org/packages/e7/3b/52badfb2aecec2c377ddf1ae75f55db3ba2d321c5e164f14461c90837ef3/xxhash-3.6.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4f6f72232f849eb9d0141e2ebe2677ece15adfd0fa599bc058aad83c714bb2c6", size = 213074, upload-time = "2025-10-02T14:35:42.29Z" }, + { url = "https://files.pythonhosted.org/packages/a2/2b/ae46b4e9b92e537fa30d03dbc19cdae57ed407e9c26d163895e968e3de85/xxhash-3.6.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:63275a8aba7865e44b1813d2177e0f5ea7eadad3dd063a21f7cf9afdc7054063", size = 212388, upload-time = "2025-10-02T14:35:43.929Z" }, + { url = "https://files.pythonhosted.org/packages/f5/80/49f88d3afc724b4ac7fbd664c8452d6db51b49915be48c6982659e0e7942/xxhash-3.6.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cd01fa2aa00d8b017c97eb46b9a794fbdca53fc14f845f5a328c71254b0abb7", size = 445614, upload-time = "2025-10-02T14:35:45.216Z" }, + { url = "https://files.pythonhosted.org/packages/ed/ba/603ce3961e339413543d8cd44f21f2c80e2a7c5cfe692a7b1f2cccf58f3c/xxhash-3.6.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0226aa89035b62b6a86d3c68df4d7c1f47a342b8683da2b60cedcddb46c4d95b", size = 194024, upload-time = "2025-10-02T14:35:46.959Z" }, + { url = "https://files.pythonhosted.org/packages/78/d1/8e225ff7113bf81545cfdcd79eef124a7b7064a0bba53605ff39590b95c2/xxhash-3.6.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c6e193e9f56e4ca4923c61238cdaced324f0feac782544eb4c6d55ad5cc99ddd", size = 210541, upload-time = "2025-10-02T14:35:48.301Z" }, + { url = "https://files.pythonhosted.org/packages/6f/58/0f89d149f0bad89def1a8dd38feb50ccdeb643d9797ec84707091d4cb494/xxhash-3.6.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9176dcaddf4ca963d4deb93866d739a343c01c969231dbe21680e13a5d1a5bf0", size = 198305, upload-time = "2025-10-02T14:35:49.584Z" }, + { url = "https://files.pythonhosted.org/packages/11/38/5eab81580703c4df93feb5f32ff8fa7fe1e2c51c1f183ee4e48d4bb9d3d7/xxhash-3.6.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c1ce4009c97a752e682b897aa99aef84191077a9433eb237774689f14f8ec152", size = 210848, upload-time = "2025-10-02T14:35:50.877Z" }, + { url = "https://files.pythonhosted.org/packages/5e/6b/953dc4b05c3ce678abca756416e4c130d2382f877a9c30a20d08ee6a77c0/xxhash-3.6.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:8cb2f4f679b01513b7adbb9b1b2f0f9cdc31b70007eaf9d59d0878809f385b11", size = 414142, upload-time = "2025-10-02T14:35:52.15Z" }, + { url = "https://files.pythonhosted.org/packages/08/a9/238ec0d4e81a10eb5026d4a6972677cbc898ba6c8b9dbaec12ae001b1b35/xxhash-3.6.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:653a91d7c2ab54a92c19ccf43508b6a555440b9be1bc8be553376778be7f20b5", size = 191547, upload-time = "2025-10-02T14:35:53.547Z" }, + { url = "https://files.pythonhosted.org/packages/f1/ee/3cf8589e06c2164ac77c3bf0aa127012801128f1feebf2a079272da5737c/xxhash-3.6.0-cp314-cp314-win32.whl", hash = "sha256:a756fe893389483ee8c394d06b5ab765d96e68fbbfe6fde7aa17e11f5720559f", size = 31214, upload-time = "2025-10-02T14:35:54.746Z" }, + { url = "https://files.pythonhosted.org/packages/02/5d/a19552fbc6ad4cb54ff953c3908bbc095f4a921bc569433d791f755186f1/xxhash-3.6.0-cp314-cp314-win_amd64.whl", hash = "sha256:39be8e4e142550ef69629c9cd71b88c90e9a5db703fecbcf265546d9536ca4ad", size = 32290, upload-time = "2025-10-02T14:35:55.791Z" }, + { url = "https://files.pythonhosted.org/packages/b1/11/dafa0643bc30442c887b55baf8e73353a344ee89c1901b5a5c54a6c17d39/xxhash-3.6.0-cp314-cp314-win_arm64.whl", hash = "sha256:25915e6000338999236f1eb68a02a32c3275ac338628a7eaa5a269c401995679", size = 28795, upload-time = "2025-10-02T14:35:57.162Z" }, + { url = "https://files.pythonhosted.org/packages/2c/db/0e99732ed7f64182aef4a6fb145e1a295558deec2a746265dcdec12d191e/xxhash-3.6.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c5294f596a9017ca5a3e3f8884c00b91ab2ad2933cf288f4923c3fd4346cf3d4", size = 32955, upload-time = "2025-10-02T14:35:58.267Z" }, + { url = "https://files.pythonhosted.org/packages/55/f4/2a7c3c68e564a099becfa44bb3d398810cc0ff6749b0d3cb8ccb93f23c14/xxhash-3.6.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1cf9dcc4ab9cff01dfbba78544297a3a01dafd60f3bde4e2bfd016cf7e4ddc67", size = 31072, upload-time = "2025-10-02T14:35:59.382Z" }, + { url = "https://files.pythonhosted.org/packages/c6/d9/72a29cddc7250e8a5819dad5d466facb5dc4c802ce120645630149127e73/xxhash-3.6.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:01262da8798422d0685f7cef03b2bd3f4f46511b02830861df548d7def4402ad", size = 196579, upload-time = "2025-10-02T14:36:00.838Z" }, + { url = "https://files.pythonhosted.org/packages/63/93/b21590e1e381040e2ca305a884d89e1c345b347404f7780f07f2cdd47ef4/xxhash-3.6.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51a73fb7cb3a3ead9f7a8b583ffd9b8038e277cdb8cb87cf890e88b3456afa0b", size = 215854, upload-time = "2025-10-02T14:36:02.207Z" }, + { url = "https://files.pythonhosted.org/packages/ce/b8/edab8a7d4fa14e924b29be877d54155dcbd8b80be85ea00d2be3413a9ed4/xxhash-3.6.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b9c6df83594f7df8f7f708ce5ebeacfc69f72c9fbaaababf6cf4758eaada0c9b", size = 214965, upload-time = "2025-10-02T14:36:03.507Z" }, + { url = "https://files.pythonhosted.org/packages/27/67/dfa980ac7f0d509d54ea0d5a486d2bb4b80c3f1bb22b66e6a05d3efaf6c0/xxhash-3.6.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:627f0af069b0ea56f312fd5189001c24578868643203bca1abbc2c52d3a6f3ca", size = 448484, upload-time = "2025-10-02T14:36:04.828Z" }, + { url = "https://files.pythonhosted.org/packages/8c/63/8ffc2cc97e811c0ca5d00ab36604b3ea6f4254f20b7bc658ca825ce6c954/xxhash-3.6.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aa912c62f842dfd013c5f21a642c9c10cd9f4c4e943e0af83618b4a404d9091a", size = 196162, upload-time = "2025-10-02T14:36:06.182Z" }, + { url = "https://files.pythonhosted.org/packages/4b/77/07f0e7a3edd11a6097e990f6e5b815b6592459cb16dae990d967693e6ea9/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b465afd7909db30168ab62afe40b2fcf79eedc0b89a6c0ab3123515dc0df8b99", size = 213007, upload-time = "2025-10-02T14:36:07.733Z" }, + { url = "https://files.pythonhosted.org/packages/ae/d8/bc5fa0d152837117eb0bef6f83f956c509332ce133c91c63ce07ee7c4873/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:a881851cf38b0a70e7c4d3ce81fc7afd86fbc2a024f4cfb2a97cf49ce04b75d3", size = 200956, upload-time = "2025-10-02T14:36:09.106Z" }, + { url = "https://files.pythonhosted.org/packages/26/a5/d749334130de9411783873e9b98ecc46688dad5db64ca6e04b02acc8b473/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:9b3222c686a919a0f3253cfc12bb118b8b103506612253b5baeaac10d8027cf6", size = 213401, upload-time = "2025-10-02T14:36:10.585Z" }, + { url = "https://files.pythonhosted.org/packages/89/72/abed959c956a4bfc72b58c0384bb7940663c678127538634d896b1195c10/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:c5aa639bc113e9286137cec8fadc20e9cd732b2cc385c0b7fa673b84fc1f2a93", size = 417083, upload-time = "2025-10-02T14:36:12.276Z" }, + { url = "https://files.pythonhosted.org/packages/0c/b3/62fd2b586283b7d7d665fb98e266decadf31f058f1cf6c478741f68af0cb/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5c1343d49ac102799905e115aee590183c3921d475356cb24b4de29a4bc56518", size = 193913, upload-time = "2025-10-02T14:36:14.025Z" }, + { url = "https://files.pythonhosted.org/packages/9a/9a/c19c42c5b3f5a4aad748a6d5b4f23df3bed7ee5445accc65a0fb3ff03953/xxhash-3.6.0-cp314-cp314t-win32.whl", hash = "sha256:5851f033c3030dd95c086b4a36a2683c2ff4a799b23af60977188b057e467119", size = 31586, upload-time = "2025-10-02T14:36:15.603Z" }, + { url = "https://files.pythonhosted.org/packages/03/d6/4cc450345be9924fd5dc8c590ceda1db5b43a0a889587b0ae81a95511360/xxhash-3.6.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0444e7967dac37569052d2409b00a8860c2135cff05502df4da80267d384849f", size = 32526, upload-time = "2025-10-02T14:36:16.708Z" }, + { url = "https://files.pythonhosted.org/packages/0f/c9/7243eb3f9eaabd1a88a5a5acadf06df2d83b100c62684b7425c6a11bcaa8/xxhash-3.6.0-cp314-cp314t-win_arm64.whl", hash = "sha256:bb79b1e63f6fd84ec778a4b1916dfe0a7c3fdb986c06addd5db3a0d413819d95", size = 28898, upload-time = "2025-10-02T14:36:17.843Z" }, + { url = "https://files.pythonhosted.org/packages/93/1e/8aec23647a34a249f62e2398c42955acd9b4c6ed5cf08cbea94dc46f78d2/xxhash-3.6.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0f7b7e2ec26c1666ad5fc9dbfa426a6a3367ceaf79db5dd76264659d509d73b0", size = 30662, upload-time = "2025-10-02T14:37:01.743Z" }, + { url = "https://files.pythonhosted.org/packages/b8/0b/b14510b38ba91caf43006209db846a696ceea6a847a0c9ba0a5b1adc53d6/xxhash-3.6.0-pp311-pypy311_pp73-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5dc1e14d14fa0f5789ec29a7062004b5933964bb9b02aae6622b8f530dc40296", size = 41056, upload-time = "2025-10-02T14:37:02.879Z" }, + { url = "https://files.pythonhosted.org/packages/50/55/15a7b8a56590e66ccd374bbfa3f9ffc45b810886c8c3b614e3f90bd2367c/xxhash-3.6.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:881b47fc47e051b37d94d13e7455131054b56749b91b508b0907eb07900d1c13", size = 36251, upload-time = "2025-10-02T14:37:04.44Z" }, + { url = "https://files.pythonhosted.org/packages/62/b2/5ac99a041a29e58e95f907876b04f7067a0242cb85b5f39e726153981503/xxhash-3.6.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c6dc31591899f5e5666f04cc2e529e69b4072827085c1ef15294d91a004bc1bd", size = 32481, upload-time = "2025-10-02T14:37:05.869Z" }, + { url = "https://files.pythonhosted.org/packages/7b/d9/8d95e906764a386a3d3b596f3c68bb63687dfca806373509f51ce8eea81f/xxhash-3.6.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:15e0dac10eb9309508bfc41f7f9deaa7755c69e35af835db9cb10751adebc35d", size = 31565, upload-time = "2025-10-02T14:37:06.966Z" }, +] From 9409d5ea31ed1f68cc59dd9dfa59e2efe6973aa8 Mon Sep 17 00:00:00 2001 From: Lukas Kikuchi Date: Fri, 1 May 2026 13:09:12 +0100 Subject: [PATCH 14/15] fix(netrun-ui): sync config field registrations with refactored netrun MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prompt to recreate this work: The netrun-ui CI tests `test_config_schema.py::TestRegistrationCompleteness` detect drift between the Pydantic config models in `netrun` and the field registrations in `netrun-ui/src/lib/configFieldRegistrations.ts`. After the major netrun refactor (Phases 0-5), three registrations are missing and six are stale. Steps: 1. In `netrun-ui/src/lib/configFieldRegistrations.ts`: - Remove stale registrations: * `NetConfig.dead_letter_path` * `NetConfig.dead_letter_callback` * `NodeExecutionConfig.defer_net_actions` * `NodeExecutionConfig.capture_prints` * `NodeExecutionConfig.print_flush_interval` * `NodeExecutionConfig.print_buffer_max_size` - Add missing registrations (all `'auto'`): * `NetConfig.resources` * `NodeExecutionConfig.depends_on` * `NodeExecutionConfig.resources` 2. In `netrun-ui/tests/test_config_schema.py::test_complex_field`, `dead_letter_callback` no longer exists. Replace it with `default_signals` (a `dict[str, ...]` field still on NetConfig that classifies as `FieldCategory.COMPLEX`). 3. Verify: `cd netrun-ui && uv run pytest tests/test_config_schema.py -q` — all 47 tests should pass. Co-Authored-By: Claude Opus 4.7 (1M context) --- netrun-ui/src/lib/configFieldRegistrations.ts | 9 +++------ netrun-ui/tests/test_config_schema.py | 2 +- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/netrun-ui/src/lib/configFieldRegistrations.ts b/netrun-ui/src/lib/configFieldRegistrations.ts index 147bec51..7083f07f 100644 --- a/netrun-ui/src/lib/configFieldRegistrations.ts +++ b/netrun-ui/src/lib/configFieldRegistrations.ts @@ -24,9 +24,8 @@ registerField('NetConfig', 'extra', 'ignore'); registerField('NetConfig', 'default_pool_allocation_method', 'auto'); registerField('NetConfig', 'node_vars', 'custom', 'NodeVariablesSection.svelte'); registerField('NetConfig', 'dead_letter_queue', 'auto'); -registerField('NetConfig', 'dead_letter_path', 'auto'); -registerField('NetConfig', 'dead_letter_callback', 'custom', 'NetSettingsSection.svelte'); registerField('NetConfig', 'output_queues', 'custom', 'OutputQueuesSection.svelte'); +registerField('NetConfig', 'resources', 'auto'); registerField('NetConfig', 'error_on_undeclared_output', 'auto'); registerField('NetConfig', 'type_checking_enabled', 'auto'); registerField('NetConfig', 'propagate_exceptions', 'auto'); @@ -74,14 +73,12 @@ registerField('NodeExecutionConfig', 'defer_init', 'auto'); registerField('NodeExecutionConfig', 'max_parallel_epochs', 'auto'); registerField('NodeExecutionConfig', 'max_epochs', 'auto'); registerField('NodeExecutionConfig', 'rate_limit_per_second', 'auto'); -registerField('NodeExecutionConfig', 'defer_net_actions', 'auto'); registerField('NodeExecutionConfig', 'retries', 'auto'); registerField('NodeExecutionConfig', 'retry_wait', 'auto'); registerField('NodeExecutionConfig', 'timeout', 'auto'); -registerField('NodeExecutionConfig', 'capture_prints', 'auto'); -registerField('NodeExecutionConfig', 'print_flush_interval', 'auto'); -registerField('NodeExecutionConfig', 'print_buffer_max_size', 'auto'); registerField('NodeExecutionConfig', 'print_echo_stdout', 'auto'); +registerField('NodeExecutionConfig', 'depends_on', 'auto'); +registerField('NodeExecutionConfig', 'resources', 'auto'); registerField('NodeExecutionConfig', 'pool_allocation_method', 'auto'); registerField('NodeExecutionConfig', 'node_vars', 'custom', 'NodeVariablesSection.svelte'); registerField('NodeExecutionConfig', 'type_checking_enabled', 'auto'); diff --git a/netrun-ui/tests/test_config_schema.py b/netrun-ui/tests/test_config_schema.py index bcd7a72e..612e6bc1 100644 --- a/netrun-ui/tests/test_config_schema.py +++ b/netrun-ui/tests/test_config_schema.py @@ -100,7 +100,7 @@ def test_float_or_null_field(self): def test_complex_field(self): schema = get_model_schema(NetConfig, "NetConfig") - field = next(f for f in schema.fields if f.name == "dead_letter_callback") + field = next(f for f in schema.fields if f.name == "default_signals") assert field.category == FieldCategory.COMPLEX def test_str_or_null_field(self): From a120791a5df409fa49da4007a67f2aa58e2b568b Mon Sep 17 00:00:00 2001 From: Lukas Kikuchi Date: Sat, 2 May 2026 19:58:36 +0100 Subject: [PATCH 15/15] fix(netrun): address bugs and footguns found in PR #40 verification MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Empirically verify every Phase 0–5 feature in PR #40 against the live runtime (parallel verification subagents driving repro scripts), then fix the issues that surfaced: 1. ctx.state did not persist across multiprocess/remote retries — every retry sent the parent's still-empty dict because worker mutations never round-tripped back. Add a `state` field to NodeExecutionResult, populate it in `_get_execution_result()`, and copy it back into `_epochs[epoch_id].retry_state` after each attempt so the next retry sees prior-attempt mutations. 2. Non-picklable exceptions raised in worker code (or carried in NodeExecutionResult.exception) silently hung the parent on multiprocess pools — `channel.send(e)` blew up on PicklingError, the bare-except swallowed it, and no terminal UP_RUN_RESPONSE arrived. Add `to_picklable_exception()` helper that returns the original `e` if picklable or a `RuntimeError` shim that preserves the type name and repr; apply it in both EM worker error paths and the preprocessor wrapper that catches user exceptions. Replace the silent `except: pass` with a logged error so future failures are diagnosable. 3. `netrun validate` reported broken factory imports as warnings with exit 0, hiding real config breakage in CI. Promote resolution failures to `errors` with non-zero exit. Also temporarily add the config's `project_root_path` to `sys.path` during resolution so factories resolve the same way they will at runtime, and clean matching modules out of `sys.modules` afterwards so back-to-back validates of different sample projects don't pick up cached symbols. 4. Disabled-dep silent hang — when a node's `depends_on` referenced a node with `enabled=False`, the dependent waited forever with no diagnostic. Emit a logging.WARNING at Net construction listing dependents of disabled nodes (so users running with controls can still enable at runtime). 5. Undeclared-resource silent skip — `_get_allowed_epochs` short-circuited the resource check when `NetConfig.resources` was empty, ignoring undeclared node-level `resources` keys. Validate at Net construction that every node-level resource name is declared in `NetConfig.resources`; remove the now-unreachable runtime guard. Adds regression tests for each fix: - test_ctx_state_persists_across_retries_multiprocess - test_unpicklable_exception_does_not_hang - test_validate_reports_resolution_failure (updated to assert exit≠0) - test_depends_on_disabled_node_warns - test_resources_undefined_raises (updated for construction-time check) Tests: netrun 1308/1308 (was 1305 + 3 new), netrun-cli 61/61. Samples 14/15/16/17 run cleanly on the fix branch. Co-Authored-By: Claude Opus 4.7 (1M context) --- netrun-cli/nbs/netrun_cli/02_commands.ipynb | 31 ++++- netrun-cli/nbs/tests/test_cli.ipynb | 19 ++- netrun-cli/pts/netrun_cli/02_commands.pct.py | 31 ++++- netrun-cli/pts/tests/test_cli.pct.py | 19 ++- netrun-cli/src/netrun_cli/_commands.py | 31 ++++- netrun-cli/src/tests/test_cli.py | 19 ++- netrun/pts/netrun/05_execution_manager.pct.py | 57 +++++++-- .../netrun/06_net/01_net/00_context.pct.py | 34 ++++-- netrun/pts/netrun/06_net/01_net/02_net.pct.py | 49 ++++++-- ...test_execution_manager_multiprocess.pct.py | 41 +++++++ .../tests/05_execution_manager/workers.pct.py | 15 +++ netrun/pts/tests/06_net/test_net.pct.py | 113 +++++++++++++++++- netrun/src/netrun/execution_manager.py | 72 ++++++++--- netrun/src/netrun/net/_net/_context.py | 34 ++++-- netrun/src/netrun/net/_net/_net.py | 49 ++++++-- .../test_execution_manager_multiprocess.py | 36 +++++- netrun/src/tests/execution_manager/workers.py | 17 ++- netrun/src/tests/net/test_net.py | 112 +++++++++++++++-- 18 files changed, 658 insertions(+), 121 deletions(-) diff --git a/netrun-cli/nbs/netrun_cli/02_commands.ipynb b/netrun-cli/nbs/netrun_cli/02_commands.ipynb index c2ca88e5..6be8c32e 100644 --- a/netrun-cli/nbs/netrun_cli/02_commands.ipynb +++ b/netrun-cli/nbs/netrun_cli/02_commands.ipynb @@ -102,12 +102,39 @@ " for err in config_errors:\n", " errors.append(f\"{err.type}: {err.msg}\")\n", "\n", - " # Step 3: Post-resolution validation via Rust (only if resolve succeeds)\n", + " # Step 3: Post-resolution validation via Rust (only if resolve succeeds).\n", + " # Temporarily add the config's project_root_path to sys.path so dotted\n", + " # imports relative to the project root (e.g. \"nodes.double\") resolve the\n", + " # same way they will at runtime under Net.\n", + " import sys as _sys\n", + " project_root = str(net_config.project_root_path)\n", + " _added_to_path = False\n", + " if project_root and project_root not in _sys.path:\n", + " _sys.path.insert(0, project_root)\n", + " _added_to_path = True\n", " try:\n", " resolved = net_config.resolve()\n", " except Exception as e:\n", + " # Resolution failures (broken factory imports, missing files, etc.)\n", + " # are real errors — they would crash any actual run. Surface them with\n", + " # a non-zero exit so CI/UI can act on them.\n", " resolved = None\n", - " warnings.append(f\"Resolution error: {e}\")\n", + " errors.append(f\"Resolution error: {e}\")\n", + " finally:\n", + " if _added_to_path:\n", + " try:\n", + " _sys.path.remove(project_root)\n", + " except ValueError:\n", + " pass\n", + " # Drop modules imported from this project so a subsequent validate\n", + " # of a different config doesn't get a cached version with the wrong\n", + " # symbols (matters for tests that validate multiple sample projects\n", + " # in one process).\n", + " for mod_name in list(_sys.modules):\n", + " mod = _sys.modules[mod_name]\n", + " mod_file = getattr(mod, \"__file__\", None)\n", + " if mod_file and mod_file.startswith(project_root):\n", + " del _sys.modules[mod_name]\n", "\n", " if resolved is not None:\n", " try:\n", diff --git a/netrun-cli/nbs/tests/test_cli.ipynb b/netrun-cli/nbs/tests/test_cli.ipynb index 4682f5fc..e9f30f24 100644 --- a/netrun-cli/nbs/tests/test_cli.ipynb +++ b/netrun-cli/nbs/tests/test_cli.ipynb @@ -554,10 +554,10 @@ "import tempfile\n", "\n", "def test_validate_reports_resolution_failure():\n", - " \"\"\"Test that validate reports resolution errors instead of silently swallowing them.\n", + " \"\"\"Test that validate reports resolution errors as hard failures.\n", "\n", - " Regression test: netrun validate suppresses resolution failures by catching\n", - " all exceptions and setting resolved=None, reporting valid=True.\n", + " Regression test: an earlier version routed broken factory imports to\n", + " `warnings` and returned exit 0, hiding real config breakage in CI.\n", " \"\"\"\n", " config_data = {\n", " \"graph\": {\n", @@ -582,17 +582,12 @@ " f.flush()\n", " result = runner.invoke(app, [\"validate\", \"-c\", f.name])\n", "\n", - " # Resolution failures are reported as warnings (not errors, since resolution\n", - " # depends on Python path at runtime). The key requirement: the failure must\n", - " # NOT be silently swallowed — it must appear in the output.\n", + " assert result.exit_code != 0, f\"validate must return non-zero on resolution failure, got: {result.stdout}\"\n", " data = json.loads(result.stdout)\n", - " warnings = data.get(\"warnings\", [])\n", + " assert data.get(\"valid\") is False\n", " errors = data.get(\"errors\", [])\n", - " all_messages = warnings + errors\n", - " assert any(\n", - " \"resolution\" in str(m).lower() or \"module\" in str(m).lower()\n", - " for m in all_messages\n", - " ), f\"validate silently swallowed resolution failure: {data}\"" + " assert any(\"Resolution error\" in str(e) for e in errors), \\\n", + " f\"resolution failure must appear in errors (not warnings): {data}\"" ], "execution_count": null, "outputs": [], diff --git a/netrun-cli/pts/netrun_cli/02_commands.pct.py b/netrun-cli/pts/netrun_cli/02_commands.pct.py index 1687ef14..c1999d07 100644 --- a/netrun-cli/pts/netrun_cli/02_commands.pct.py +++ b/netrun-cli/pts/netrun_cli/02_commands.pct.py @@ -80,12 +80,39 @@ def validate( for err in config_errors: errors.append(f"{err.type}: {err.msg}") - # Step 3: Post-resolution validation via Rust (only if resolve succeeds) + # Step 3: Post-resolution validation via Rust (only if resolve succeeds). + # Temporarily add the config's project_root_path to sys.path so dotted + # imports relative to the project root (e.g. "nodes.double") resolve the + # same way they will at runtime under Net. + import sys as _sys + project_root = str(net_config.project_root_path) + _added_to_path = False + if project_root and project_root not in _sys.path: + _sys.path.insert(0, project_root) + _added_to_path = True try: resolved = net_config.resolve() except Exception as e: + # Resolution failures (broken factory imports, missing files, etc.) + # are real errors — they would crash any actual run. Surface them with + # a non-zero exit so CI/UI can act on them. resolved = None - warnings.append(f"Resolution error: {e}") + errors.append(f"Resolution error: {e}") + finally: + if _added_to_path: + try: + _sys.path.remove(project_root) + except ValueError: + pass + # Drop modules imported from this project so a subsequent validate + # of a different config doesn't get a cached version with the wrong + # symbols (matters for tests that validate multiple sample projects + # in one process). + for mod_name in list(_sys.modules): + mod = _sys.modules[mod_name] + mod_file = getattr(mod, "__file__", None) + if mod_file and mod_file.startswith(project_root): + del _sys.modules[mod_name] if resolved is not None: try: diff --git a/netrun-cli/pts/tests/test_cli.pct.py b/netrun-cli/pts/tests/test_cli.pct.py index 5c9e1771..2bc4619e 100644 --- a/netrun-cli/pts/tests/test_cli.pct.py +++ b/netrun-cli/pts/tests/test_cli.pct.py @@ -420,10 +420,10 @@ def test_help(): import tempfile def test_validate_reports_resolution_failure(): - """Test that validate reports resolution errors instead of silently swallowing them. + """Test that validate reports resolution errors as hard failures. - Regression test: netrun validate suppresses resolution failures by catching - all exceptions and setting resolved=None, reporting valid=True. + Regression test: an earlier version routed broken factory imports to + `warnings` and returned exit 0, hiding real config breakage in CI. """ config_data = { "graph": { @@ -448,17 +448,12 @@ def test_validate_reports_resolution_failure(): f.flush() result = runner.invoke(app, ["validate", "-c", f.name]) - # Resolution failures are reported as warnings (not errors, since resolution - # depends on Python path at runtime). The key requirement: the failure must - # NOT be silently swallowed — it must appear in the output. + assert result.exit_code != 0, f"validate must return non-zero on resolution failure, got: {result.stdout}" data = json.loads(result.stdout) - warnings = data.get("warnings", []) + assert data.get("valid") is False errors = data.get("errors", []) - all_messages = warnings + errors - assert any( - "resolution" in str(m).lower() or "module" in str(m).lower() - for m in all_messages - ), f"validate silently swallowed resolution failure: {data}" + assert any("Resolution error" in str(e) for e in errors), \ + f"resolution failure must appear in errors (not warnings): {data}" # %% [markdown] # ## Test node --edges diff --git a/netrun-cli/src/netrun_cli/_commands.py b/netrun-cli/src/netrun_cli/_commands.py index fb2308c4..f757e282 100644 --- a/netrun-cli/src/netrun_cli/_commands.py +++ b/netrun-cli/src/netrun_cli/_commands.py @@ -61,12 +61,39 @@ def validate( for err in config_errors: errors.append(f"{err.type}: {err.msg}") - # Step 3: Post-resolution validation via Rust (only if resolve succeeds) + # Step 3: Post-resolution validation via Rust (only if resolve succeeds). + # Temporarily add the config's project_root_path to sys.path so dotted + # imports relative to the project root (e.g. "nodes.double") resolve the + # same way they will at runtime under Net. + import sys as _sys + project_root = str(net_config.project_root_path) + _added_to_path = False + if project_root and project_root not in _sys.path: + _sys.path.insert(0, project_root) + _added_to_path = True try: resolved = net_config.resolve() except Exception as e: + # Resolution failures (broken factory imports, missing files, etc.) + # are real errors — they would crash any actual run. Surface them with + # a non-zero exit so CI/UI can act on them. resolved = None - warnings.append(f"Resolution error: {e}") + errors.append(f"Resolution error: {e}") + finally: + if _added_to_path: + try: + _sys.path.remove(project_root) + except ValueError: + pass + # Drop modules imported from this project so a subsequent validate + # of a different config doesn't get a cached version with the wrong + # symbols (matters for tests that validate multiple sample projects + # in one process). + for mod_name in list(_sys.modules): + mod = _sys.modules[mod_name] + mod_file = getattr(mod, "__file__", None) + if mod_file and mod_file.startswith(project_root): + del _sys.modules[mod_name] if resolved is not None: try: diff --git a/netrun-cli/src/tests/test_cli.py b/netrun-cli/src/tests/test_cli.py index 4202e01a..35a5057b 100644 --- a/netrun-cli/src/tests/test_cli.py +++ b/netrun-cli/src/tests/test_cli.py @@ -359,10 +359,10 @@ def test_help(): import tempfile def test_validate_reports_resolution_failure(): - """Test that validate reports resolution errors instead of silently swallowing them. + """Test that validate reports resolution errors as hard failures. - Regression test: netrun validate suppresses resolution failures by catching - all exceptions and setting resolved=None, reporting valid=True. + Regression test: an earlier version routed broken factory imports to + `warnings` and returned exit 0, hiding real config breakage in CI. """ config_data = { "graph": { @@ -387,17 +387,12 @@ def test_validate_reports_resolution_failure(): f.flush() result = runner.invoke(app, ["validate", "-c", f.name]) - # Resolution failures are reported as warnings (not errors, since resolution - # depends on Python path at runtime). The key requirement: the failure must - # NOT be silently swallowed — it must appear in the output. + assert result.exit_code != 0, f"validate must return non-zero on resolution failure, got: {result.stdout}" data = json.loads(result.stdout) - warnings = data.get("warnings", []) + assert data.get("valid") is False errors = data.get("errors", []) - all_messages = warnings + errors - assert any( - "resolution" in str(m).lower() or "module" in str(m).lower() - for m in all_messages - ), f"validate silently swallowed resolution failure: {data}" + assert any("Resolution error" in str(e) for e in errors), \ + f"resolution failure must appear in errors (not warnings): {data}" # %% pts/tests/test_cli.pct.py 29 def test_node_edges_incoming(): diff --git a/netrun/pts/netrun/05_execution_manager.pct.py b/netrun/pts/netrun/05_execution_manager.pct.py index 9c573cad..bcadc67f 100644 --- a/netrun/pts/netrun/05_execution_manager.pct.py +++ b/netrun/pts/netrun/05_execution_manager.pct.py @@ -27,6 +27,7 @@ from netrun._iutils import get_timestamp_utc from datetime import datetime import asyncio +import logging import threading from enum import Enum from dataclasses import dataclass @@ -119,6 +120,28 @@ def _convert_to_str_if_not_serializable(obj: Any) -> tuple[bool, Any]: except (pickle.PicklingError, TypeError, AttributeError): return (True, str(obj)) +# %% +#|export +def to_picklable_exception(e: BaseException) -> BaseException: + """Return `e` unchanged if it's pickle-serializable, otherwise a serializable + proxy. + + Worker error paths must always send a terminal `UP_RUN_RESPONSE` so the client + doesn't hang. If the user (or some library) raises an exception that holds an + unpicklable attribute (lambda, generator, traceback, open file handle, …), the + raw exception cannot cross a multiprocess boundary. We replace it with a + `RuntimeError` whose message preserves the original type name and `repr`, so + the caller still gets an `Exception` it can `raise`. + """ + try: + pickle.dumps(e) + return e + except (pickle.PicklingError, TypeError, AttributeError) as pickle_err: + return RuntimeError( + f" " + f"(pickle error: {pickle_err!r})" + ) + # %% [markdown] # ## Workers # @@ -218,14 +241,25 @@ def _worker_func( channel.send(ExecutionManagerProtocolKeys.UP_RUN_RESPONSE.value, (msg_id, timestamp_utc_started, timestamp_utc_completed, converted_to_str, _res)) except Exception as e: - # Send error response so the client doesn't hang forever + # Send error response so the client doesn't hang forever. + # Sanitize first: a non-picklable exception would otherwise + # blow up channel.send() across a multiprocess boundary, the + # except below would swallow the PicklingError, and the caller + # would block forever waiting for UP_RUN_RESPONSE. timestamp_utc_error = get_timestamp_utc() if timestamp_utc_started is None: timestamp_utc_started = timestamp_utc_error + err = to_picklable_exception(e) try: - channel.send(ExecutionManagerProtocolKeys.UP_RUN_RESPONSE.value, (msg_id, timestamp_utc_started, timestamp_utc_error, False, e)) - except Exception: - pass # Worker loop must survive + channel.send(ExecutionManagerProtocolKeys.UP_RUN_RESPONSE.value, (msg_id, timestamp_utc_started, timestamp_utc_error, False, err)) + except Exception as send_err: + # Last-ditch: the channel itself is broken. Log so this + # doesn't manifest as a silent hang. + _logger = logging.getLogger("netrun.execution_manager") + _logger.error( + "Worker %s failed to deliver error response for run %s: %r", + worker_id, run_id, send_err, + ) # SEND_FUNCTION elif key == ExecutionManagerProtocolKeys.SEND_FUNCTION.value: msg_id, func_key, func = data @@ -278,12 +312,19 @@ async def _handle_run(msg_id, func, args, kwargs): await channel.send(ExecutionManagerProtocolKeys.UP_RUN_RESPONSE.value, (msg_id, timestamp_utc_started, timestamp_utc_completed, converted_to_str, _res)) except Exception as e: - # Send error response so the client doesn't hang forever + # Send error response so the client doesn't hang forever. + # See to_picklable_exception() — sanitize before sending across + # process boundaries. timestamp_utc_error = get_timestamp_utc() + err = to_picklable_exception(e) try: - await channel.send(ExecutionManagerProtocolKeys.UP_RUN_RESPONSE.value, (msg_id, timestamp_utc_started, timestamp_utc_error, False, e)) - except Exception: - pass # If we can't send the error, the client will eventually timeout + await channel.send(ExecutionManagerProtocolKeys.UP_RUN_RESPONSE.value, (msg_id, timestamp_utc_started, timestamp_utc_error, False, err)) + except Exception as send_err: + _logger = logging.getLogger("netrun.execution_manager") + _logger.error( + "Async worker %s failed to deliver error response for msg %s: %r", + worker_id, msg_id, send_err, + ) pending_tasks: set[asyncio.Task] = set() diff --git a/netrun/pts/netrun/06_net/01_net/00_context.pct.py b/netrun/pts/netrun/06_net/01_net/00_context.pct.py index cc9ee01b..394b5dbd 100644 --- a/netrun/pts/netrun/06_net/01_net/00_context.pct.py +++ b/netrun/pts/netrun/06_net/01_net/00_context.pct.py @@ -36,7 +36,7 @@ from netrun.net.config._nodes import resolve_effective_exec_field, INHERITABLE_EXEC_FIELDS from netrun._iutils import get_timestamp_utc from netrun.packets import LazyPacketValueSpec -from netrun.execution_manager import _worker_state +from netrun.execution_manager import _worker_state, to_picklable_exception # %% [markdown] # ## Structured Logging Data Models @@ -384,14 +384,17 @@ class NodeExecutionContext: Mutable retry state via `ctx.state`: `state` is a per-epoch dict that persists across retry attempts. Use it to cache expensive precomputation (e.g. loaded DataFrames, parsed - configs) so retries don't redo the work. The same dict instance is - reused on every retry; it is cleared when the epoch succeeds or - permanently fails. Unlike the deferred packet operations, mutations - to `state` survive failed attempts. - - Locality: the dict lives wherever the node runs (thread, process, or - remote worker). For multiprocess/remote pools, each worker process - keeps its own copy — values must be picklable if you set them. + configs) so retries don't redo the work. Mutations made during an + attempt are visible to the next retry of the same epoch; the dict is + cleared when the epoch succeeds or permanently fails. Unlike the + deferred packet operations, mutations to `state` survive failed + attempts. + + Locality: on thread pools the same dict instance is shared by + reference. On multiprocess/remote pools the parent round-trips the + post-attempt state back through `NodeExecutionResult.state`, so + retries observe the same logical state — but values must be picklable + and large objects pay the serialization cost on every attempt. Example: async def main(ctx, ad_ids): @@ -791,6 +794,7 @@ def _get_execution_result(self) -> "NodeExecutionResult": created_packets=self._created_packets.copy(), consumed_packets=self._consumed_packets.copy(), structured_log_buffer=self._structured_log_buffer.copy(), + state=dict(self.state), ) # %% [markdown] @@ -831,6 +835,10 @@ class NodeExecutionResult: exception: Exception | None = None """Exception raised by the node function (if any).""" + state: dict[str, Any] = field(default_factory=dict) + """Final value of ctx.state after the attempt. Round-tripped to the parent + so retries on multiprocess/remote pools see prior-attempt mutations.""" + # %% [markdown] # ## NodeFailureContext # @@ -1358,7 +1366,9 @@ async def wrapped( except EpochCancelled: pass except Exception as e: - exception = e + # Sanitize so the resulting NodeExecutionResult is always + # picklable across multiprocess/remote pool boundaries. + exception = to_picklable_exception(e) return preprocessor_self._build_result(ctx, func_result, exception) @@ -1408,7 +1418,9 @@ def wrapped( except EpochCancelled: pass except Exception as e: - exception = e + # Sanitize so the resulting NodeExecutionResult is always + # picklable across multiprocess/remote pool boundaries. + exception = to_picklable_exception(e) return preprocessor_self._build_result(ctx, func_result, exception) diff --git a/netrun/pts/netrun/06_net/01_net/02_net.pct.py b/netrun/pts/netrun/06_net/01_net/02_net.pct.py index aec9309b..0b621636 100644 --- a/netrun/pts/netrun/06_net/01_net/02_net.pct.py +++ b/netrun/pts/netrun/06_net/01_net/02_net.pct.py @@ -517,6 +517,34 @@ def _check_cycle(node: str) -> None: if not exec_config.enabled: self._disabled_nodes.add(node_name) + # Validate node-level resources references against NetConfig.resources. + # Without this check the resource gate in _get_allowed_epochs() is + # silently skipped when NetConfig.resources is empty/None, so undeclared + # resource keys are ignored at runtime. + for node_name, exec_config in self._node_execution_configs.items(): + if exec_config.resources: + for res_name in exec_config.resources: + if res_name not in self._resource_capacities: + raise ValueError( + f"Node '{node_name}' requires resource '{res_name}' " + f"which is not defined in NetConfig.resources" + ) + + # Warn if any node depends_on a node that is disabled at construction + # time. The dependent will hang forever unless the dependency is + # enabled at runtime via a control epoch — the user is unlikely to + # want this silently. + for node_name, exec_config in self._node_execution_configs.items(): + if exec_config.depends_on: + disabled_deps = [d for d in exec_config.depends_on if d in self._disabled_nodes] + if disabled_deps: + import logging + logging.getLogger("netrun.net").warning( + "Node '%s' depends_on disabled node(s) %s — '%s' will not run " + "until those are enabled at runtime.", + node_name, disabled_deps, node_name, + ) + # Control handler dispatch table self._control_handlers: dict[str, Callable] = { "start_epoch": self._control_start_epoch, @@ -1963,16 +1991,14 @@ def _filter_startable_epochs( if not all(dep in self._node_has_completed for dep in config.depends_on): continue # Skip — dependency not yet satisfied - # Check resources: all required resource slots must be available - if config and config.resources and self._resource_capacities: + # Check resources: all required resource slots must be available. + # Construction-time validation (in __init__) guarantees every + # res_name here is declared in NetConfig.resources, so the + # capacity lookup below cannot return None. + if config and config.resources: can_acquire = True for res_name, needed in config.resources.items(): - capacity = self._resource_capacities.get(res_name) - if capacity is None: - raise ValueError( - f"Node '{node_name}' requires resource '{res_name}' " - f"which is not defined in NetConfig.resources" - ) + capacity = self._resource_capacities[res_name] current_usage = ( self._resource_usage.get(res_name, 0) + new_resource_usage.get(res_name, 0) @@ -2474,6 +2500,13 @@ async def _execute_epoch_with_retry( # Extract NodeExecutionResult from job result execution_result: NodeExecutionResult = job_result.result + # Round-trip ctx.state mutations back into retry_state so the next retry + # observes them on cross-process pools (thread pools share the dict by + # reference, but multiprocess/remote workers operate on a pickled copy). + retry_state = self._epochs[epoch_id].retry_state + retry_state.clear() + retry_state.update(execution_result.state) + # Record which pool/worker ran this epoch self._epochs[epoch_id].pool_id = job_result.pool_id self._epochs[epoch_id].worker_id = job_result.worker_id diff --git a/netrun/pts/tests/05_execution_manager/test_execution_manager_multiprocess.pct.py b/netrun/pts/tests/05_execution_manager/test_execution_manager_multiprocess.pct.py index 1d455284..be441947 100644 --- a/netrun/pts/tests/05_execution_manager/test_execution_manager_multiprocess.pct.py +++ b/netrun/pts/tests/05_execution_manager/test_execution_manager_multiprocess.pct.py @@ -31,6 +31,7 @@ multiply_numbers, slow_function, function_with_kwargs, + function_with_unpicklable_error, async_add, mp_stdout_function, ) @@ -695,5 +696,45 @@ async def test_flush_all_pool_stdout_raises_for_non_multiprocess(): with pytest.raises(ValueError, match="not a MultiprocessPool"): await manager.flush_all_pool_stdout("thread_pool") +# %% [markdown] +# ## Regression: non-picklable exception must not hang the worker + +# %% +#|export +@pytest.mark.asyncio +async def test_unpicklable_exception_does_not_hang(): + """Bug #2 regression: a worker raising an exception with non-picklable + state used to deadlock the parent because the inner channel.send(e) blew + up on PicklingError, the bare-except swallowed it, and no terminal + UP_RUN_RESPONSE ever arrived. The fix sanitises the exception via + `to_picklable_exception()` before sending. The caller should now see a + `RuntimeError` carrying the original type name, in well under the test + timeout.""" + manager = ExecutionManager({ + "pool": (MultiprocessPool, {"num_processes": 1, "threads_per_process": 1}), + }) + + async with manager: + with pytest.raises(Exception) as excinfo: + await asyncio.wait_for( + manager.run( + pool_id="pool", + worker_id=0, + func_import_path_or_key="tests.execution_manager.workers.function_with_unpicklable_error", + func_args=(), + func_kwargs={}, + ), + timeout=10.0, + ) + + # The original BadExc isn't picklable, so the worker substitutes a + # RuntimeError that preserves the type name and message. + assert "_UnpicklableExc" in str(excinfo.value) or "unpicklable" in str(excinfo.value).lower() + assert not isinstance(excinfo.value, asyncio.TimeoutError), \ + "manager.run() must not hang on non-picklable exceptions" + +# %% +await test_unpicklable_exception_does_not_hang(); + # %% await test_flush_all_pool_stdout_raises_for_non_multiprocess(); diff --git a/netrun/pts/tests/05_execution_manager/workers.pct.py b/netrun/pts/tests/05_execution_manager/workers.pct.py index 7793b8a2..9b701b7e 100644 --- a/netrun/pts/tests/05_execution_manager/workers.pct.py +++ b/netrun/pts/tests/05_execution_manager/workers.pct.py @@ -49,6 +49,21 @@ def function_with_error() -> None: raise ValueError("Intentional error") +def function_with_unpicklable_error() -> None: + """A function that raises an exception holding an unpicklable attribute. + + Used to regression-test Bug #2: a worker error path that sends `e` raw + over a multiprocess channel hangs forever when pickle fails. + """ + class _UnpicklableExc(Exception): + def __init__(self): + super().__init__("intentional unpicklable error") + # Lambdas cannot be pickled; this is the same trap real code hits + # via traceback objects, generators, open files, … + self.bad = lambda: None + raise _UnpicklableExc() + + def function_returns_non_serializable(): """A function that returns something non-serializable.""" return lambda x: x # Lambdas can't be pickled diff --git a/netrun/pts/tests/06_net/test_net.pct.py b/netrun/pts/tests/06_net/test_net.pct.py index 417de9e8..dd875267 100644 --- a/netrun/pts/tests/06_net/test_net.pct.py +++ b/netrun/pts/tests/06_net/test_net.pct.py @@ -7526,6 +7526,62 @@ def stateful_node(ctx, packets): # %% asyncio.get_event_loop().run_until_complete(test_ctx_state_cleared_on_success()) +# %% [markdown] +# ### Bug #1 regression: ctx.state across MP retries +# +# `ctx.state` mutations made inside a multiprocess worker live in a pickled +# copy of the dict; without the round-trip fix, the parent's view stays empty +# and every retry sees `count == 1` forever. The node below increments a +# counter until 3, so a clean run requires that mutations survive across +# retries on a multiprocess pool. + +# %% +#|export +def _mp_ctx_state_increment_until_three(ctx, x: int, print) -> int: + """Module-level node used by test_ctx_state_persists_across_retries_multiprocess. + + Picklable for `MultiprocessPool` because it isn't a closure. Increments + `ctx.state["count"]` and fails until count reaches 3. + """ + count = ctx.state.get("count", 0) + 1 + ctx.state["count"] = count + print(f"attempt {count}") + if count < 3: + raise RuntimeError(f"forced fail at count={count}") + return count + +# %% +#|export +@pytest.mark.asyncio +async def test_ctx_state_persists_across_retries_multiprocess(): + """Bug #1 regression: ctx.state must survive cross-process retries.""" + config = NetConfig.model_validate({ + "pools": {"procs": {"spec": {"type": "multiprocess", "num_processes": 1, "threads_per_process": 1}}}, + "output_queues": {"results": {"ports": [["pipeline", "out"]]}}, + "graph": { + "nodes": [ + { + "name": "pipeline", + "factory": "netrun.node_factories.from_function", + "factory_args": {"func": "tests.net.test_net._mp_ctx_state_increment_until_three"}, + "execution_config": {"pools": ["procs"], "retries": 5, "retry_wait": 0.0, + "propagate_exceptions": False}, + } + ], + "edges": [], + }, + }) + async with Net(config) as net: + net.inject_data("pipeline", "x", [1]) + await net.run_until_blocked() + results = net.flush_output_queue("results") + assert results == [3], f"expected [3] (state persisted across MP retries), got {results}" + # No DLQ entry — the node ultimately succeeded + assert len(net._dead_letter_queue) == 0 + +# %% +asyncio.get_event_loop().run_until_complete(test_ctx_state_persists_across_retries_multiprocess()) + # %% #|export @pytest.mark.asyncio @@ -7766,10 +7822,11 @@ def simple_node(ctx, packets): graph=graph_config, ) - async with Net(config) as net: - net.inject_data("BadNode", "in", [1]) - with pytest.raises(ValueError, match="nonexistent_resource"): - await net.run_until_blocked() + # Validation now fires at Net construction so undeclared resource refs + # cannot silently pass the runtime gate when NetConfig.resources is empty + # or contains different names. + with pytest.raises(ValueError, match="nonexistent_resource"): + Net(config) # %% asyncio.get_event_loop().run_until_complete(test_resources_undefined_raises()) @@ -7866,3 +7923,51 @@ def noop(ctx, packets): # %% test_depends_on_invalid_node_raises() + +# %% +#|export +def test_depends_on_disabled_node_warns(caplog): + """Footgun #4 regression: depending on a disabled node must surface a + warning at construction time so the dependent doesn't silently hang.""" + import logging + def noop(ctx, packets): + pass + + def make_node(name, *, enabled=True, depends_on=None): + return NodeConfig( + name=name, + in_ports={"in": PortConfig()}, + in_salvo_conditions={ + "default": SalvoConditionConfig( + max_salvos=MaxSalvosFiniteConfig(max=1), + ports={"in": PacketCountAllConfig()}, + term=SalvoConditionTermPortConfig( + port_name="in", + state=PortStateNonEmptyConfig(), + ), + ), + }, + execution_config=NodeExecutionConfig( + node_name=name, + pools=["main"], + exec_node_func=noop, + enabled=enabled, + depends_on=depends_on, + ), + ) + + config = NetConfig( + pools={"main": PoolConfig(spec=MainPoolConfig())}, + graph=GraphConfig( + nodes=[make_node("A", enabled=False), make_node("B", depends_on=["A"])], + edges=[], + ), + ) + + with caplog.at_level(logging.WARNING, logger="netrun.net"): + Net(config) + + matching = [r for r in caplog.records if "depends_on disabled node" in r.getMessage()] + assert matching, f"expected a warning about disabled deps, got: {[r.getMessage() for r in caplog.records]}" + assert "B" in matching[0].getMessage() + assert "A" in matching[0].getMessage() diff --git a/netrun/src/netrun/execution_manager.py b/netrun/src/netrun/execution_manager.py index 5b438ac9..c4edc812 100644 --- a/netrun/src/netrun/execution_manager.py +++ b/netrun/src/netrun/execution_manager.py @@ -1,6 +1,6 @@ # AUTOGENERATED! DO NOT EDIT! File to edit: pts/netrun/05_execution_manager.pct.py -__all__ = ['ExecutionManager', 'ExecutionManagerProtocolKeys', 'JobResult', 'PoolType', 'RunAllocationMethod', 'SubmittedJobInfo', 'create_execution_manager_server', 'remote_execution_manager_worker'] +__all__ = ['ExecutionManager', 'ExecutionManagerProtocolKeys', 'JobResult', 'PoolType', 'RunAllocationMethod', 'SubmittedJobInfo', 'create_execution_manager_server', 'remote_execution_manager_worker', 'to_picklable_exception'] # %% pts/netrun/05_execution_manager.pct.py 3 from typing import Any @@ -8,6 +8,7 @@ from ._iutils import get_timestamp_utc from datetime import datetime import asyncio +import logging import threading from enum import Enum from dataclasses import dataclass @@ -85,7 +86,28 @@ def _convert_to_str_if_not_serializable(obj: Any) -> tuple[bool, Any]: except (pickle.PicklingError, TypeError, AttributeError): return (True, str(obj)) -# %% pts/netrun/05_execution_manager.pct.py 12 +# %% pts/netrun/05_execution_manager.pct.py 11 +def to_picklable_exception(e: BaseException) -> BaseException: + """Return `e` unchanged if it's pickle-serializable, otherwise a serializable + proxy. + + Worker error paths must always send a terminal `UP_RUN_RESPONSE` so the client + doesn't hang. If the user (or some library) raises an exception that holds an + unpicklable attribute (lambda, generator, traceback, open file handle, …), the + raw exception cannot cross a multiprocess boundary. We replace it with a + `RuntimeError` whose message preserves the original type name and `repr`, so + the caller still gets an `Exception` it can `raise`. + """ + try: + pickle.dumps(e) + return e + except (pickle.PicklingError, TypeError, AttributeError) as pickle_err: + return RuntimeError( + f" " + f"(pickle error: {pickle_err!r})" + ) + +# %% pts/netrun/05_execution_manager.pct.py 13 class ExecutionManagerProtocolKeys(Enum): RUN = "exec-manager:run" """ @@ -111,7 +133,7 @@ class ExecutionManagerProtocolKeys(Enum): Args: msg_id """ -# %% pts/netrun/05_execution_manager.pct.py 13 +# %% pts/netrun/05_execution_manager.pct.py 14 def _worker_func( is_in_main_process: bool, channel, @@ -177,14 +199,25 @@ def _worker_func( channel.send(ExecutionManagerProtocolKeys.UP_RUN_RESPONSE.value, (msg_id, timestamp_utc_started, timestamp_utc_completed, converted_to_str, _res)) except Exception as e: - # Send error response so the client doesn't hang forever + # Send error response so the client doesn't hang forever. + # Sanitize first: a non-picklable exception would otherwise + # blow up channel.send() across a multiprocess boundary, the + # except below would swallow the PicklingError, and the caller + # would block forever waiting for UP_RUN_RESPONSE. timestamp_utc_error = get_timestamp_utc() if timestamp_utc_started is None: timestamp_utc_started = timestamp_utc_error + err = to_picklable_exception(e) try: - channel.send(ExecutionManagerProtocolKeys.UP_RUN_RESPONSE.value, (msg_id, timestamp_utc_started, timestamp_utc_error, False, e)) - except Exception: - pass # Worker loop must survive + channel.send(ExecutionManagerProtocolKeys.UP_RUN_RESPONSE.value, (msg_id, timestamp_utc_started, timestamp_utc_error, False, err)) + except Exception as send_err: + # Last-ditch: the channel itself is broken. Log so this + # doesn't manifest as a silent hang. + _logger = logging.getLogger("netrun.execution_manager") + _logger.error( + "Worker %s failed to deliver error response for run %s: %r", + worker_id, run_id, send_err, + ) # SEND_FUNCTION elif key == ExecutionManagerProtocolKeys.SEND_FUNCTION.value: msg_id, func_key, func = data @@ -193,7 +226,7 @@ def _worker_func( else: raise ValueError(f"Unknown execution manager protocol key: '{key}'.") -# %% pts/netrun/05_execution_manager.pct.py 14 +# %% pts/netrun/05_execution_manager.pct.py 15 async def _async_worker_func( channel, worker_id, @@ -236,12 +269,19 @@ async def _handle_run(msg_id, func, args, kwargs): await channel.send(ExecutionManagerProtocolKeys.UP_RUN_RESPONSE.value, (msg_id, timestamp_utc_started, timestamp_utc_completed, converted_to_str, _res)) except Exception as e: - # Send error response so the client doesn't hang forever + # Send error response so the client doesn't hang forever. + # See to_picklable_exception() — sanitize before sending across + # process boundaries. timestamp_utc_error = get_timestamp_utc() + err = to_picklable_exception(e) try: - await channel.send(ExecutionManagerProtocolKeys.UP_RUN_RESPONSE.value, (msg_id, timestamp_utc_started, timestamp_utc_error, False, e)) - except Exception: - pass # If we can't send the error, the client will eventually timeout + await channel.send(ExecutionManagerProtocolKeys.UP_RUN_RESPONSE.value, (msg_id, timestamp_utc_started, timestamp_utc_error, False, err)) + except Exception as send_err: + _logger = logging.getLogger("netrun.execution_manager") + _logger.error( + "Async worker %s failed to deliver error response for msg %s: %r", + worker_id, msg_id, send_err, + ) pending_tasks: set[asyncio.Task] = set() @@ -279,7 +319,7 @@ async def _handle_run(msg_id, func, args, kwargs): if pending_tasks: await asyncio.gather(*pending_tasks, return_exceptions=True) -# %% pts/netrun/05_execution_manager.pct.py 16 +# %% pts/netrun/05_execution_manager.pct.py 17 def remote_execution_manager_worker( channel, worker_id: int, @@ -296,7 +336,7 @@ def remote_execution_manager_worker( shared_loop=shared_loop, ) -# %% pts/netrun/05_execution_manager.pct.py 17 +# %% pts/netrun/05_execution_manager.pct.py 18 def create_execution_manager_server( worker_name: str = "execution_manager", func_preprocessor: Callable | None = None, @@ -366,7 +406,7 @@ def create_execution_manager_server( server.register_worker(worker_name, worker_fn) return server -# %% pts/netrun/05_execution_manager.pct.py 19 +# %% pts/netrun/05_execution_manager.pct.py 20 @dataclass class JobResult: """Result of a job execution.""" @@ -395,7 +435,7 @@ class RunAllocationMethod(Enum): RANDOM = "random" LEAST_BUSY = "least-busy" -# %% pts/netrun/05_execution_manager.pct.py 20 +# %% pts/netrun/05_execution_manager.pct.py 21 PoolType = ThreadPool | MultiprocessPool | SingleWorkerPool | RemotePoolClient class ExecutionManager: diff --git a/netrun/src/netrun/net/_net/_context.py b/netrun/src/netrun/net/_net/_context.py index 5aa29ac8..baa2b1a2 100644 --- a/netrun/src/netrun/net/_net/_context.py +++ b/netrun/src/netrun/net/_net/_context.py @@ -18,7 +18,7 @@ from ...net.config._nodes import resolve_effective_exec_field, INHERITABLE_EXEC_FIELDS from ..._iutils import get_timestamp_utc from ...packets import LazyPacketValueSpec -from ...execution_manager import _worker_state +from ...execution_manager import _worker_state, to_picklable_exception # %% pts/netrun/06_net/01_net/00_context.pct.py 5 @dataclass @@ -347,14 +347,17 @@ class NodeExecutionContext: Mutable retry state via `ctx.state`: `state` is a per-epoch dict that persists across retry attempts. Use it to cache expensive precomputation (e.g. loaded DataFrames, parsed - configs) so retries don't redo the work. The same dict instance is - reused on every retry; it is cleared when the epoch succeeds or - permanently fails. Unlike the deferred packet operations, mutations - to `state` survive failed attempts. - - Locality: the dict lives wherever the node runs (thread, process, or - remote worker). For multiprocess/remote pools, each worker process - keeps its own copy — values must be picklable if you set them. + configs) so retries don't redo the work. Mutations made during an + attempt are visible to the next retry of the same epoch; the dict is + cleared when the epoch succeeds or permanently fails. Unlike the + deferred packet operations, mutations to `state` survive failed + attempts. + + Locality: on thread pools the same dict instance is shared by + reference. On multiprocess/remote pools the parent round-trips the + post-attempt state back through `NodeExecutionResult.state`, so + retries observe the same logical state — but values must be picklable + and large objects pay the serialization cost on every attempt. Example: async def main(ctx, ad_ids): @@ -754,6 +757,7 @@ def _get_execution_result(self) -> "NodeExecutionResult": created_packets=self._created_packets.copy(), consumed_packets=self._consumed_packets.copy(), structured_log_buffer=self._structured_log_buffer.copy(), + state=dict(self.state), ) # %% pts/netrun/06_net/01_net/00_context.pct.py 11 @@ -788,6 +792,10 @@ class NodeExecutionResult: exception: Exception | None = None """Exception raised by the node function (if any).""" + state: dict[str, Any] = field(default_factory=dict) + """Final value of ctx.state after the attempt. Round-tripped to the parent + so retries on multiprocess/remote pools see prior-attempt mutations.""" + # %% pts/netrun/06_net/01_net/00_context.pct.py 13 @dataclass class NodeFailureContext: @@ -1283,7 +1291,9 @@ async def wrapped( except EpochCancelled: pass except Exception as e: - exception = e + # Sanitize so the resulting NodeExecutionResult is always + # picklable across multiprocess/remote pool boundaries. + exception = to_picklable_exception(e) return preprocessor_self._build_result(ctx, func_result, exception) @@ -1333,7 +1343,9 @@ def wrapped( except EpochCancelled: pass except Exception as e: - exception = e + # Sanitize so the resulting NodeExecutionResult is always + # picklable across multiprocess/remote pool boundaries. + exception = to_picklable_exception(e) return preprocessor_self._build_result(ctx, func_result, exception) diff --git a/netrun/src/netrun/net/_net/_net.py b/netrun/src/netrun/net/_net/_net.py index 5c4e17c0..f43d2003 100644 --- a/netrun/src/netrun/net/_net/_net.py +++ b/netrun/src/netrun/net/_net/_net.py @@ -491,6 +491,34 @@ def _check_cycle(node: str) -> None: if not exec_config.enabled: self._disabled_nodes.add(node_name) + # Validate node-level resources references against NetConfig.resources. + # Without this check the resource gate in _get_allowed_epochs() is + # silently skipped when NetConfig.resources is empty/None, so undeclared + # resource keys are ignored at runtime. + for node_name, exec_config in self._node_execution_configs.items(): + if exec_config.resources: + for res_name in exec_config.resources: + if res_name not in self._resource_capacities: + raise ValueError( + f"Node '{node_name}' requires resource '{res_name}' " + f"which is not defined in NetConfig.resources" + ) + + # Warn if any node depends_on a node that is disabled at construction + # time. The dependent will hang forever unless the dependency is + # enabled at runtime via a control epoch — the user is unlikely to + # want this silently. + for node_name, exec_config in self._node_execution_configs.items(): + if exec_config.depends_on: + disabled_deps = [d for d in exec_config.depends_on if d in self._disabled_nodes] + if disabled_deps: + import logging + logging.getLogger("netrun.net").warning( + "Node '%s' depends_on disabled node(s) %s — '%s' will not run " + "until those are enabled at runtime.", + node_name, disabled_deps, node_name, + ) + # Control handler dispatch table self._control_handlers: dict[str, Callable] = { "start_epoch": self._control_start_epoch, @@ -1937,16 +1965,14 @@ def _filter_startable_epochs( if not all(dep in self._node_has_completed for dep in config.depends_on): continue # Skip — dependency not yet satisfied - # Check resources: all required resource slots must be available - if config and config.resources and self._resource_capacities: + # Check resources: all required resource slots must be available. + # Construction-time validation (in __init__) guarantees every + # res_name here is declared in NetConfig.resources, so the + # capacity lookup below cannot return None. + if config and config.resources: can_acquire = True for res_name, needed in config.resources.items(): - capacity = self._resource_capacities.get(res_name) - if capacity is None: - raise ValueError( - f"Node '{node_name}' requires resource '{res_name}' " - f"which is not defined in NetConfig.resources" - ) + capacity = self._resource_capacities[res_name] current_usage = ( self._resource_usage.get(res_name, 0) + new_resource_usage.get(res_name, 0) @@ -2448,6 +2474,13 @@ async def _execute_epoch_with_retry( # Extract NodeExecutionResult from job result execution_result: NodeExecutionResult = job_result.result + # Round-trip ctx.state mutations back into retry_state so the next retry + # observes them on cross-process pools (thread pools share the dict by + # reference, but multiprocess/remote workers operate on a pickled copy). + retry_state = self._epochs[epoch_id].retry_state + retry_state.clear() + retry_state.update(execution_result.state) + # Record which pool/worker ran this epoch self._epochs[epoch_id].pool_id = job_result.pool_id self._epochs[epoch_id].worker_id = job_result.worker_id diff --git a/netrun/src/tests/execution_manager/test_execution_manager_multiprocess.py b/netrun/src/tests/execution_manager/test_execution_manager_multiprocess.py index b1421e31..47327992 100644 --- a/netrun/src/tests/execution_manager/test_execution_manager_multiprocess.py +++ b/netrun/src/tests/execution_manager/test_execution_manager_multiprocess.py @@ -1,6 +1,6 @@ # AUTOGENERATED! DO NOT EDIT! File to edit: pts/tests/05_execution_manager/test_execution_manager_multiprocess.pct.py -__all__ = ['test_allocation_with_specific_workers', 'test_async_function', 'test_concurrent_jobs', 'test_context_manager', 'test_double_start_raises', 'test_empty_workers_raises', 'test_flush_all_pool_stdout', 'test_flush_all_pool_stdout_raises_for_non_multiprocess', 'test_flush_pool_stdout', 'test_flush_pool_stdout_raises_for_non_multiprocess', 'test_function_with_kwargs', 'test_get_num_workers', 'test_get_process_ids', 'test_get_process_ids_raises_for_non_multiprocess', 'test_get_worker_jobs_empty', 'test_immediate_close', 'test_job_result_timestamps', 'test_multiple_pools', 'test_pool_ids', 'test_random_allocation', 'test_round_robin_allocation', 'test_send_function_and_run', 'test_send_function_to_pool', 'test_start_and_close'] +__all__ = ['test_allocation_with_specific_workers', 'test_async_function', 'test_concurrent_jobs', 'test_context_manager', 'test_double_start_raises', 'test_empty_workers_raises', 'test_flush_all_pool_stdout', 'test_flush_all_pool_stdout_raises_for_non_multiprocess', 'test_flush_pool_stdout', 'test_flush_pool_stdout_raises_for_non_multiprocess', 'test_function_with_kwargs', 'test_get_num_workers', 'test_get_process_ids', 'test_get_process_ids_raises_for_non_multiprocess', 'test_get_worker_jobs_empty', 'test_immediate_close', 'test_job_result_timestamps', 'test_multiple_pools', 'test_pool_ids', 'test_random_allocation', 'test_round_robin_allocation', 'test_send_function_and_run', 'test_send_function_to_pool', 'test_start_and_close', 'test_unpicklable_exception_does_not_hang'] # %% pts/tests/05_execution_manager/test_execution_manager_multiprocess.pct.py 2 import pytest @@ -20,6 +20,7 @@ multiply_numbers, slow_function, function_with_kwargs, + function_with_unpicklable_error, async_add, mp_stdout_function, ) @@ -557,3 +558,36 @@ async def test_flush_all_pool_stdout_raises_for_non_multiprocess(): async with manager: with pytest.raises(ValueError, match="not a MultiprocessPool"): await manager.flush_all_pool_stdout("thread_pool") + +# %% pts/tests/05_execution_manager/test_execution_manager_multiprocess.pct.py 62 +@pytest.mark.asyncio +async def test_unpicklable_exception_does_not_hang(): + """Bug #2 regression: a worker raising an exception with non-picklable + state used to deadlock the parent because the inner channel.send(e) blew + up on PicklingError, the bare-except swallowed it, and no terminal + UP_RUN_RESPONSE ever arrived. The fix sanitises the exception via + `to_picklable_exception()` before sending. The caller should now see a + `RuntimeError` carrying the original type name, in well under the test + timeout.""" + manager = ExecutionManager({ + "pool": (MultiprocessPool, {"num_processes": 1, "threads_per_process": 1}), + }) + + async with manager: + with pytest.raises(Exception) as excinfo: + await asyncio.wait_for( + manager.run( + pool_id="pool", + worker_id=0, + func_import_path_or_key="tests.execution_manager.workers.function_with_unpicklable_error", + func_args=(), + func_kwargs={}, + ), + timeout=10.0, + ) + + # The original BadExc isn't picklable, so the worker substitutes a + # RuntimeError that preserves the type name and message. + assert "_UnpicklableExc" in str(excinfo.value) or "unpicklable" in str(excinfo.value).lower() + assert not isinstance(excinfo.value, asyncio.TimeoutError), \ + "manager.run() must not hang on non-picklable exceptions" diff --git a/netrun/src/tests/execution_manager/workers.py b/netrun/src/tests/execution_manager/workers.py index ecb51f72..c837aa87 100644 --- a/netrun/src/tests/execution_manager/workers.py +++ b/netrun/src/tests/execution_manager/workers.py @@ -1,6 +1,6 @@ # AUTOGENERATED! DO NOT EDIT! File to edit: pts/tests/05_execution_manager/workers.pct.py -__all__ = ['add_numbers', 'async_add', 'async_subprocess_function', 'function_returns_non_serializable', 'function_with_error', 'function_with_kwargs', 'function_with_multiple_prints', 'function_with_print', 'mp_stdout_function', 'multiply_numbers', 'nested_async_function', 'slow_function', 'slow_printing_function'] +__all__ = ['add_numbers', 'async_add', 'async_subprocess_function', 'function_returns_non_serializable', 'function_with_error', 'function_with_kwargs', 'function_with_multiple_prints', 'function_with_print', 'function_with_unpicklable_error', 'mp_stdout_function', 'multiply_numbers', 'nested_async_function', 'slow_function', 'slow_printing_function'] # %% pts/tests/05_execution_manager/workers.pct.py 2 import asyncio @@ -35,6 +35,21 @@ def function_with_error() -> None: raise ValueError("Intentional error") +def function_with_unpicklable_error() -> None: + """A function that raises an exception holding an unpicklable attribute. + + Used to regression-test Bug #2: a worker error path that sends `e` raw + over a multiprocess channel hangs forever when pickle fails. + """ + class _UnpicklableExc(Exception): + def __init__(self): + super().__init__("intentional unpicklable error") + # Lambdas cannot be pickled; this is the same trap real code hits + # via traceback objects, generators, open files, … + self.bad = lambda: None + raise _UnpicklableExc() + + def function_returns_non_serializable(): """A function that returns something non-serializable.""" return lambda x: x # Lambdas can't be pickled diff --git a/netrun/src/tests/net/test_net.py b/netrun/src/tests/net/test_net.py index 5a9a7909..f520dc42 100644 --- a/netrun/src/tests/net/test_net.py +++ b/netrun/src/tests/net/test_net.py @@ -1,6 +1,6 @@ # AUTOGENERATED! DO NOT EDIT! File to edit: pts/tests/06_net/test_net.pct.py -__all__ = ['create_simple_graph_config', 'create_simple_net_config', 'test_async_epoch_callback', 'test_async_error_on_thread_pool', 'test_async_exec_func_on_main_pool', 'test_async_exec_func_on_thread_pool', 'test_async_from_function_factory_on_thread_pool', 'test_async_retry_on_thread_pool', 'test_async_subprocess_on_thread_pool', 'test_check_type_distinguishes_modes', 'test_check_type_string_generic_fails', 'test_check_type_string_simple', 'test_check_type_type_object_generic', 'test_check_type_type_object_generic_mismatch', 'test_check_type_type_object_simple', 'test_close_node_func_called_on_net_stop', 'test_concurrent_async_siblings_on_main_pool', 'test_context_cancel_epoch', 'test_context_consume_packet', 'test_context_consume_packet_not_found', 'test_context_create_packet', 'test_context_create_packet_from_value_func', 'test_context_full_workflow', 'test_context_get_execution_result', 'test_context_load_output_port', 'test_context_print_accumulates', 'test_context_print_basic', 'test_context_print_custom_separators', 'test_context_print_echo_stdout', 'test_context_print_empty', 'test_context_print_flush_ignored', 'test_context_print_multiple_timestamps', 'test_context_print_non_string', 'test_context_send_output_salvo', 'test_create_func_preprocessor_from_config', 'test_create_func_preprocessor_from_config_empty', 'test_create_net_func_preprocessor_basic', 'test_create_net_func_preprocessor_captures_exception', 'test_create_net_func_preprocessor_captures_prints', 'test_create_net_func_preprocessor_with_retry_info', 'test_ctx_state_cleared_on_success', 'test_ctx_state_empty_by_default', 'test_ctx_state_persists_across_retries', 'test_ctx_vars_access', 'test_ctx_vars_empty', 'test_ctx_vars_inherit_missing_global_raises', 'test_ctx_vars_merging', 'test_ctx_vars_merging_with_inherit', 'test_dead_letter_queue_after_max_retries', 'test_dead_letter_queue_has_pool_and_worker', 'test_defer_init_delays_init_node_func', 'test_deferred_actions_preserved_in_result', 'test_deferred_queue_add_consume_packet', 'test_deferred_queue_add_create_packet', 'test_deferred_queue_add_load_output_port', 'test_deferred_queue_add_send_output_salvo', 'test_deferred_queue_discard', 'test_deferred_queue_multiple_creates', 'test_depends_on_blocks_until_dependency_completes', 'test_depends_on_circular_raises', 'test_depends_on_invalid_node_raises', 'test_depends_on_multiple_dependencies', 'test_depends_on_none_is_noop', 'test_enable_disable_unknown_node', 'test_epoch_callback_deregistration', 'test_epoch_cancelled_can_be_raised_and_caught', 'test_epoch_cancelled_exception', 'test_epoch_end_on_cancelled', 'test_epoch_error_basic', 'test_epoch_error_can_be_caught', 'test_epoch_error_chaining', 'test_epoch_error_minimal', 'test_epoch_error_str', 'test_epoch_execution_simple_node', 'test_epoch_execution_with_output', 'test_epoch_record_cancellation_lifecycle', 'test_epoch_record_lifecycle_timestamps', 'test_epoch_record_out_salvos_populated', 'test_exception_queue_contains_epoch_error', 'test_execute_node_calls_init_node_func', 'test_execute_node_inside_net_basic', 'test_execute_node_inside_net_source_node', 'test_execute_node_outside_net', 'test_execute_node_outside_net_async_func', 'test_execute_node_salvo_condition_not_satisfied', 'test_execution_manager_get_pool', 'test_factory_node_exec_func_override', 'test_faulthandler_enabled_after_init', 'test_global_max_epochs_default_unlimited', 'test_global_max_epochs_limits_all_nodes', 'test_init_node_func_called_on_net_start', 'test_init_node_func_called_once_with_defer_init', 'test_is_blocked_with_disabled_node', 'test_max_epochs_allows_up_to_limit', 'test_max_epochs_exceeded_exception', 'test_max_epochs_none_unlimited', 'test_max_epochs_one_raises_on_second', 'test_max_epochs_queued_when_not_propagating', 'test_max_parallel_epochs_limits_concurrent_starts', 'test_max_parallel_epochs_none_allows_all', 'test_max_parallel_epochs_per_node', 'test_multiple_epoch_callbacks', 'test_net_check_rate_limit_enforced', 'test_net_check_rate_limit_no_config', 'test_net_check_rate_limit_none_limit', 'test_net_check_rate_limit_window_expires', 'test_net_clear_dead_letter_queue', 'test_net_close_sync', 'test_net_config_default_pool_allocation_method', 'test_net_config_default_pool_allocation_method_default', 'test_net_config_property', 'test_net_config_type_checking', 'test_net_context_manager', 'test_net_creation', 'test_net_dead_letter_queue_empty', 'test_net_dead_letter_queue_returns_copy', 'test_net_from_file_json', 'test_net_from_file_not_found', 'test_net_from_file_unsupported_format', 'test_net_get_epoch_log_empty', 'test_net_get_node_logs_empty', 'test_net_get_startable_epochs', 'test_net_graph_property', 'test_net_handle_print_buffer', 'test_net_init_sync', 'test_net_install_sigint_handler', 'test_net_invalid_pool_type_raises', 'test_net_is_blocked_empty_network', 'test_net_is_blocked_with_running_epochs', 'test_net_pause_and_resume', 'test_net_run_step', 'test_net_run_until_blocked', 'test_net_running_epochs_empty', 'test_net_start_and_stop', 'test_net_start_background', 'test_net_start_background_already_running', 'test_net_start_twice_raises', 'test_net_with_multiple_pool_types', 'test_net_with_node_execution_configs', 'test_node_disable_at_runtime', 'test_node_disabled_via_config', 'test_node_enable_at_runtime', 'test_node_execution_config_defaults', 'test_node_execution_config_max_epochs_default', 'test_node_execution_config_max_epochs_set', 'test_node_execution_config_new_fields', 'test_node_execution_config_retry_defaults', 'test_node_execution_config_type_checking', 'test_node_execution_config_with_retries', 'test_node_execution_context_creation', 'test_node_execution_context_with_retry_info', 'test_node_execution_result_with_exception', 'test_node_execution_result_with_func_result', 'test_node_failure_context_creation', 'test_node_failure_context_full', 'test_node_failure_context_has_pool_and_worker', 'test_node_failure_context_print', 'test_node_level_override_print', 'test_node_level_override_propagate', 'test_node_scoped_epoch_callbacks', 'test_on_epoch_end_callback', 'test_on_epoch_start_callback', 'test_on_node_failure_callback', 'test_per_node_max_epochs_overrides_global', 'test_per_node_minus_one_overrides_global_limit', 'test_pool_server_context_as_context_manager_still_works', 'test_pool_server_context_log_no_file', 'test_pool_server_context_start_stop', 'test_pool_server_context_start_stop_with_log_file', 'test_preprocessor_handles_cancel_epoch', 'test_preprocessor_type_checking_inheritance', 'test_print_exceptions_includes_pool_worker', 'test_print_exceptions_true_prints_to_stderr', 'test_propagate_exceptions_false_queues', 'test_propagate_exceptions_true_raises', 'test_propagate_exceptions_wraps_in_epoch_error', 'test_request_pool_shutdown_end_to_end', 'test_request_pool_shutdown_non_remote_raises', 'test_resources_multi_slot', 'test_resources_mutual_exclusion', 'test_resources_none_is_noop', 'test_resources_undefined_raises', 'test_retry_calls_gc_collect', 'test_retry_on_failure', 'test_run_on_init_basic', 'test_run_on_init_disabled', 'test_run_on_init_error_no_valid_condition', 'test_run_on_init_outputs_flow_downstream', 'test_run_on_init_skip_via_constructor', 'test_run_on_init_skip_via_start_param', 'test_run_on_init_start_param_overrides_constructor', 'test_serve_pool_accepts_path', 'test_serve_pool_accepts_pathlib_path', 'test_serve_pool_custom_worker_name', 'test_serve_pool_end_to_end_with_net', 'test_serve_pool_log_file_writes', 'test_serve_pool_no_log_file', 'test_serve_pool_returns_pool_server_context', 'test_serve_pool_starts_and_stops_server', 'test_serve_pool_with_log_file', 'test_server_log_callback', 'test_start_stop_with_async_funcs', 'test_stop_not_called_for_unstarted_deferred_node', 'test_streaming_downstream_starts_before_slow_sibling', 'test_streaming_error_propagates', 'test_streaming_linear_pipeline', 'test_streaming_max_parallel_epochs_across_rounds', 'test_timeout_enforcement_raises_epoch_error', 'test_timeout_goes_to_dead_letter_queue', 'test_timeout_none_no_limit', 'test_type_checking_disabled'] +__all__ = ['create_simple_graph_config', 'create_simple_net_config', 'test_async_epoch_callback', 'test_async_error_on_thread_pool', 'test_async_exec_func_on_main_pool', 'test_async_exec_func_on_thread_pool', 'test_async_from_function_factory_on_thread_pool', 'test_async_retry_on_thread_pool', 'test_async_subprocess_on_thread_pool', 'test_check_type_distinguishes_modes', 'test_check_type_string_generic_fails', 'test_check_type_string_simple', 'test_check_type_type_object_generic', 'test_check_type_type_object_generic_mismatch', 'test_check_type_type_object_simple', 'test_close_node_func_called_on_net_stop', 'test_concurrent_async_siblings_on_main_pool', 'test_context_cancel_epoch', 'test_context_consume_packet', 'test_context_consume_packet_not_found', 'test_context_create_packet', 'test_context_create_packet_from_value_func', 'test_context_full_workflow', 'test_context_get_execution_result', 'test_context_load_output_port', 'test_context_print_accumulates', 'test_context_print_basic', 'test_context_print_custom_separators', 'test_context_print_echo_stdout', 'test_context_print_empty', 'test_context_print_flush_ignored', 'test_context_print_multiple_timestamps', 'test_context_print_non_string', 'test_context_send_output_salvo', 'test_create_func_preprocessor_from_config', 'test_create_func_preprocessor_from_config_empty', 'test_create_net_func_preprocessor_basic', 'test_create_net_func_preprocessor_captures_exception', 'test_create_net_func_preprocessor_captures_prints', 'test_create_net_func_preprocessor_with_retry_info', 'test_ctx_state_cleared_on_success', 'test_ctx_state_empty_by_default', 'test_ctx_state_persists_across_retries', 'test_ctx_state_persists_across_retries_multiprocess', 'test_ctx_vars_access', 'test_ctx_vars_empty', 'test_ctx_vars_inherit_missing_global_raises', 'test_ctx_vars_merging', 'test_ctx_vars_merging_with_inherit', 'test_dead_letter_queue_after_max_retries', 'test_dead_letter_queue_has_pool_and_worker', 'test_defer_init_delays_init_node_func', 'test_deferred_actions_preserved_in_result', 'test_deferred_queue_add_consume_packet', 'test_deferred_queue_add_create_packet', 'test_deferred_queue_add_load_output_port', 'test_deferred_queue_add_send_output_salvo', 'test_deferred_queue_discard', 'test_deferred_queue_multiple_creates', 'test_depends_on_blocks_until_dependency_completes', 'test_depends_on_circular_raises', 'test_depends_on_disabled_node_warns', 'test_depends_on_invalid_node_raises', 'test_depends_on_multiple_dependencies', 'test_depends_on_none_is_noop', 'test_enable_disable_unknown_node', 'test_epoch_callback_deregistration', 'test_epoch_cancelled_can_be_raised_and_caught', 'test_epoch_cancelled_exception', 'test_epoch_end_on_cancelled', 'test_epoch_error_basic', 'test_epoch_error_can_be_caught', 'test_epoch_error_chaining', 'test_epoch_error_minimal', 'test_epoch_error_str', 'test_epoch_execution_simple_node', 'test_epoch_execution_with_output', 'test_epoch_record_cancellation_lifecycle', 'test_epoch_record_lifecycle_timestamps', 'test_epoch_record_out_salvos_populated', 'test_exception_queue_contains_epoch_error', 'test_execute_node_calls_init_node_func', 'test_execute_node_inside_net_basic', 'test_execute_node_inside_net_source_node', 'test_execute_node_outside_net', 'test_execute_node_outside_net_async_func', 'test_execute_node_salvo_condition_not_satisfied', 'test_execution_manager_get_pool', 'test_factory_node_exec_func_override', 'test_faulthandler_enabled_after_init', 'test_global_max_epochs_default_unlimited', 'test_global_max_epochs_limits_all_nodes', 'test_init_node_func_called_on_net_start', 'test_init_node_func_called_once_with_defer_init', 'test_is_blocked_with_disabled_node', 'test_max_epochs_allows_up_to_limit', 'test_max_epochs_exceeded_exception', 'test_max_epochs_none_unlimited', 'test_max_epochs_one_raises_on_second', 'test_max_epochs_queued_when_not_propagating', 'test_max_parallel_epochs_limits_concurrent_starts', 'test_max_parallel_epochs_none_allows_all', 'test_max_parallel_epochs_per_node', 'test_multiple_epoch_callbacks', 'test_net_check_rate_limit_enforced', 'test_net_check_rate_limit_no_config', 'test_net_check_rate_limit_none_limit', 'test_net_check_rate_limit_window_expires', 'test_net_clear_dead_letter_queue', 'test_net_close_sync', 'test_net_config_default_pool_allocation_method', 'test_net_config_default_pool_allocation_method_default', 'test_net_config_property', 'test_net_config_type_checking', 'test_net_context_manager', 'test_net_creation', 'test_net_dead_letter_queue_empty', 'test_net_dead_letter_queue_returns_copy', 'test_net_from_file_json', 'test_net_from_file_not_found', 'test_net_from_file_unsupported_format', 'test_net_get_epoch_log_empty', 'test_net_get_node_logs_empty', 'test_net_get_startable_epochs', 'test_net_graph_property', 'test_net_handle_print_buffer', 'test_net_init_sync', 'test_net_install_sigint_handler', 'test_net_invalid_pool_type_raises', 'test_net_is_blocked_empty_network', 'test_net_is_blocked_with_running_epochs', 'test_net_pause_and_resume', 'test_net_run_step', 'test_net_run_until_blocked', 'test_net_running_epochs_empty', 'test_net_start_and_stop', 'test_net_start_background', 'test_net_start_background_already_running', 'test_net_start_twice_raises', 'test_net_with_multiple_pool_types', 'test_net_with_node_execution_configs', 'test_node_disable_at_runtime', 'test_node_disabled_via_config', 'test_node_enable_at_runtime', 'test_node_execution_config_defaults', 'test_node_execution_config_max_epochs_default', 'test_node_execution_config_max_epochs_set', 'test_node_execution_config_new_fields', 'test_node_execution_config_retry_defaults', 'test_node_execution_config_type_checking', 'test_node_execution_config_with_retries', 'test_node_execution_context_creation', 'test_node_execution_context_with_retry_info', 'test_node_execution_result_with_exception', 'test_node_execution_result_with_func_result', 'test_node_failure_context_creation', 'test_node_failure_context_full', 'test_node_failure_context_has_pool_and_worker', 'test_node_failure_context_print', 'test_node_level_override_print', 'test_node_level_override_propagate', 'test_node_scoped_epoch_callbacks', 'test_on_epoch_end_callback', 'test_on_epoch_start_callback', 'test_on_node_failure_callback', 'test_per_node_max_epochs_overrides_global', 'test_per_node_minus_one_overrides_global_limit', 'test_pool_server_context_as_context_manager_still_works', 'test_pool_server_context_log_no_file', 'test_pool_server_context_start_stop', 'test_pool_server_context_start_stop_with_log_file', 'test_preprocessor_handles_cancel_epoch', 'test_preprocessor_type_checking_inheritance', 'test_print_exceptions_includes_pool_worker', 'test_print_exceptions_true_prints_to_stderr', 'test_propagate_exceptions_false_queues', 'test_propagate_exceptions_true_raises', 'test_propagate_exceptions_wraps_in_epoch_error', 'test_request_pool_shutdown_end_to_end', 'test_request_pool_shutdown_non_remote_raises', 'test_resources_multi_slot', 'test_resources_mutual_exclusion', 'test_resources_none_is_noop', 'test_resources_undefined_raises', 'test_retry_calls_gc_collect', 'test_retry_on_failure', 'test_run_on_init_basic', 'test_run_on_init_disabled', 'test_run_on_init_error_no_valid_condition', 'test_run_on_init_outputs_flow_downstream', 'test_run_on_init_skip_via_constructor', 'test_run_on_init_skip_via_start_param', 'test_run_on_init_start_param_overrides_constructor', 'test_serve_pool_accepts_path', 'test_serve_pool_accepts_pathlib_path', 'test_serve_pool_custom_worker_name', 'test_serve_pool_end_to_end_with_net', 'test_serve_pool_log_file_writes', 'test_serve_pool_no_log_file', 'test_serve_pool_returns_pool_server_context', 'test_serve_pool_starts_and_stops_server', 'test_serve_pool_with_log_file', 'test_server_log_callback', 'test_start_stop_with_async_funcs', 'test_stop_not_called_for_unstarted_deferred_node', 'test_streaming_downstream_starts_before_slow_sibling', 'test_streaming_error_propagates', 'test_streaming_linear_pipeline', 'test_streaming_max_parallel_epochs_across_rounds', 'test_timeout_enforcement_raises_epoch_error', 'test_timeout_goes_to_dead_letter_queue', 'test_timeout_none_no_limit', 'test_type_checking_disabled'] # %% pts/tests/06_net/test_net.pct.py 2 import pytest @@ -6820,7 +6820,49 @@ def stateful_node(ctx, packets): f"retry_state should be cleared on success, got {record.retry_state}" ) -# %% pts/tests/06_net/test_net.pct.py 370 +# %% pts/tests/06_net/test_net.pct.py 371 +def _mp_ctx_state_increment_until_three(ctx, x: int, print) -> int: + """Module-level node used by test_ctx_state_persists_across_retries_multiprocess. + + Picklable for `MultiprocessPool` because it isn't a closure. Increments + `ctx.state["count"]` and fails until count reaches 3. + """ + count = ctx.state.get("count", 0) + 1 + ctx.state["count"] = count + print(f"attempt {count}") + if count < 3: + raise RuntimeError(f"forced fail at count={count}") + return count + +# %% pts/tests/06_net/test_net.pct.py 372 +@pytest.mark.asyncio +async def test_ctx_state_persists_across_retries_multiprocess(): + """Bug #1 regression: ctx.state must survive cross-process retries.""" + config = NetConfig.model_validate({ + "pools": {"procs": {"spec": {"type": "multiprocess", "num_processes": 1, "threads_per_process": 1}}}, + "output_queues": {"results": {"ports": [["pipeline", "out"]]}}, + "graph": { + "nodes": [ + { + "name": "pipeline", + "factory": "netrun.node_factories.from_function", + "factory_args": {"func": "tests.net.test_net._mp_ctx_state_increment_until_three"}, + "execution_config": {"pools": ["procs"], "retries": 5, "retry_wait": 0.0, + "propagate_exceptions": False}, + } + ], + "edges": [], + }, + }) + async with Net(config) as net: + net.inject_data("pipeline", "x", [1]) + await net.run_until_blocked() + results = net.flush_output_queue("results") + assert results == [3], f"expected [3] (state persisted across MP retries), got {results}" + # No DLQ entry — the node ultimately succeeded + assert len(net._dead_letter_queue) == 0 + +# %% pts/tests/06_net/test_net.pct.py 374 @pytest.mark.asyncio async def test_depends_on_multiple_dependencies(): """Test that a node waits for ALL dependencies to complete.""" @@ -6891,7 +6933,7 @@ def make_node(name, func, depends_on=None): assert "A" in execution_order[:c_idx], "A should complete before C" assert "B" in execution_order[:c_idx], "B should complete before C" -# %% pts/tests/06_net/test_net.pct.py 372 +# %% pts/tests/06_net/test_net.pct.py 376 @pytest.mark.asyncio async def test_depends_on_none_is_noop(): """Test that nodes without depends_on work as before.""" @@ -6938,7 +6980,7 @@ def simple_node(ctx, packets): assert executed[0] is True -# %% pts/tests/06_net/test_net.pct.py 374 +# %% pts/tests/06_net/test_net.pct.py 378 @pytest.mark.asyncio async def test_resources_multi_slot(): """Test that a multi-slot resource allows bounded concurrency.""" @@ -7006,7 +7048,7 @@ def make_gpu_node(name): # but less than 0.25s (which would indicate only 1 slot) assert total >= 0.15, f"Expected >= 0.15s for 2-slot bounded concurrency, got {total:.3f}s" -# %% pts/tests/06_net/test_net.pct.py 376 +# %% pts/tests/06_net/test_net.pct.py 380 @pytest.mark.asyncio async def test_resources_undefined_raises(): """Test that referencing an undefined resource raises an error.""" @@ -7047,12 +7089,13 @@ def simple_node(ctx, packets): graph=graph_config, ) - async with Net(config) as net: - net.inject_data("BadNode", "in", [1]) - with pytest.raises(ValueError, match="nonexistent_resource"): - await net.run_until_blocked() + # Validation now fires at Net construction so undeclared resource refs + # cannot silently pass the runtime gate when NetConfig.resources is empty + # or contains different names. + with pytest.raises(ValueError, match="nonexistent_resource"): + Net(config) -# %% pts/tests/06_net/test_net.pct.py 379 +# %% pts/tests/06_net/test_net.pct.py 383 def test_depends_on_circular_raises(): """Test that circular depends_on raises ValueError at construction.""" def noop(ctx, packets): @@ -7095,7 +7138,7 @@ def make_node(name, depends_on=None): with pytest.raises(ValueError, match="[Cc]ircular"): Net(config) -# %% pts/tests/06_net/test_net.pct.py 381 +# %% pts/tests/06_net/test_net.pct.py 385 def test_depends_on_invalid_node_raises(): """Test that depends_on referencing non-existent node raises ValueError.""" def noop(ctx, packets): @@ -7133,3 +7176,50 @@ def noop(ctx, packets): with pytest.raises(ValueError, match="NonExistentNode"): Net(config) + +# %% pts/tests/06_net/test_net.pct.py 387 +def test_depends_on_disabled_node_warns(caplog): + """Footgun #4 regression: depending on a disabled node must surface a + warning at construction time so the dependent doesn't silently hang.""" + import logging + def noop(ctx, packets): + pass + + def make_node(name, *, enabled=True, depends_on=None): + return NodeConfig( + name=name, + in_ports={"in": PortConfig()}, + in_salvo_conditions={ + "default": SalvoConditionConfig( + max_salvos=MaxSalvosFiniteConfig(max=1), + ports={"in": PacketCountAllConfig()}, + term=SalvoConditionTermPortConfig( + port_name="in", + state=PortStateNonEmptyConfig(), + ), + ), + }, + execution_config=NodeExecutionConfig( + node_name=name, + pools=["main"], + exec_node_func=noop, + enabled=enabled, + depends_on=depends_on, + ), + ) + + config = NetConfig( + pools={"main": PoolConfig(spec=MainPoolConfig())}, + graph=GraphConfig( + nodes=[make_node("A", enabled=False), make_node("B", depends_on=["A"])], + edges=[], + ), + ) + + with caplog.at_level(logging.WARNING, logger="netrun.net"): + Net(config) + + matching = [r for r in caplog.records if "depends_on disabled node" in r.getMessage()] + assert matching, f"expected a warning about disabled deps, got: {[r.getMessage() for r in caplog.records]}" + assert "B" in matching[0].getMessage() + assert "A" in matching[0].getMessage()