diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 549c2939..f0fcc38d 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -52,6 +52,10 @@ jobs: working-directory: sdk run: poetry run ruff format --check . + - name: Run mypy type checker + working-directory: sdk + run: poetry run mypy + changelog: runs-on: ubuntu-latest name: Changelog check diff --git a/sdk/CHANGELOG.md b/sdk/CHANGELOG.md index 5aec96d0..9e99ae89 100644 --- a/sdk/CHANGELOG.md +++ b/sdk/CHANGELOG.md @@ -8,6 +8,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added +- Static type checking with mypy: the `eggai` package is type-checked in CI + (`poetry run mypy`, config in `pyproject.toml`); `mypy` is a new dev dependency. - `RedisTransport`: new `group_start` subscribe option (`"$"` default, `"0"` or a stream id) chooses where a NEW consumer group starts reading, so a consumer added to a channel that already carries traffic can pick up the existing @@ -15,6 +17,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 broker starts (#260). ### Fixed +- A2A executor: the JSON-serialisation fallback caught the non-existent + `json.JSONEncodeError`, so a failing `json.dumps` raised `AttributeError` and + masked the real error. It now catches `(TypeError, ValueError)`. +- A2A executor: the "unknown skill" error path enqueued a raw dict instead of a + proper A2A message; a skill registered with `data_type=None` raised + `TypeError` when building the message and now falls back to `BaseMessage`. +- Typed subscriptions: a `data_type` whose `type` field has no default (e.g. raw + `BaseMessage`) silently dropped every message; `subscribe()` now raises + `ValueError` asking for a default discriminator. +- `Agent.subscribe()`: plugin-prefixed kwargs (e.g. `a2a_*`) on an agent whose + plugin was never initialised raised an opaque `KeyError`; now `ValueError`. - `RedisTransport`: `agent.stop()` could hang forever on Python 3.10/3.11 with redis-py >= 8. redis-py 8 sends every command through `asyncio.wait_for` (`socket_timeout` now defaults to 5s), and on CPython < 3.12 `wait_for` can @@ -38,6 +51,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 `Channel().publish()` never reached it (#264). ### Changed +- Type-annotation cleanups across transports, channel, agent and hooks to + satisfy mypy (no behaviour change). `Transport.subscribe()`'s second + parameter is now named `handler` in the abstract base and all transports. - **BREAKING: `BaseMessage.data` is now required.** The generic base previously declared `data: TData = Field(default_factory=dict)`. Pydantic does not validate defaults, so an envelope missing `data` validated cleanly on *any* diff --git a/sdk/eggai/adapters/a2a/executor.py b/sdk/eggai/adapters/a2a/executor.py index 4858c577..1193baa8 100644 --- a/sdk/eggai/adapters/a2a/executor.py +++ b/sdk/eggai/adapters/a2a/executor.py @@ -35,22 +35,23 @@ async def execute(self, request_context: RequestContext, event_queue: EventQueue if skill_name not in self.a2a_plugin.handlers: error_msg = f"Unknown skill: {skill_name}. Available skills: {list(self.a2a_plugin.handlers.keys())}" logger.error(error_msg) - await event_queue.enqueue_event({"text": error_msg}) + # Use the same A2A-message wrapper as every other response path, + # rather than enqueuing a raw dict the event queue can't model. + await self._send_agent_response(event_queue, {"error": error_msg}) return # Get handler and extract message data handler = self.a2a_plugin.handlers[skill_name] message_data = self._extract_message_data(request_context) - # Create proper BaseMessage with validated data + # Validate the raw data against the skill's declared type (if any) and + # wrap it in the BaseMessage subclass the handler expects. Looked up once; + # data_type may be None (a skill can register without one), which falls + # through to the generic BaseMessage rather than calling None(...). validated_data = message_data - if skill_name in self.a2a_plugin.data_types: - data_type = self.a2a_plugin.data_types[skill_name] - if ( - data_type - and hasattr(data_type, "__args__") - and len(data_type.__args__) > 0 - ): + data_type = self.a2a_plugin.data_types.get(skill_name) + if data_type is not None: + if hasattr(data_type, "__args__") and len(data_type.__args__) > 0: # Extract inner type from BaseMessage[T] and validate the raw data inner_type = data_type.__args__[0] if hasattr(inner_type, "model_validate"): @@ -64,10 +65,6 @@ async def execute(self, request_context: RequestContext, event_queue: EventQueue f"Failed to validate data as {inner_type.__name__}: {e}" ) # Keep as dict if validation fails - - # Create the proper BaseMessage structure that the handler expects - if skill_name in self.a2a_plugin.data_types: - data_type = self.a2a_plugin.data_types[skill_name] # Create instance of the specific BaseMessage subclass eggai_message = data_type( source="a2a-client", @@ -216,6 +213,6 @@ async def _send_agent_response(self, event_queue: EventQueue, data: dict): # Fallback: try to send as simple event try: await event_queue.enqueue_event({"text": json.dumps(data)}) - except (json.JSONEncodeError, TypeError) as json_err: + except (TypeError, ValueError) as json_err: logger.error(f"Failed to serialize response data: {json_err}") raise diff --git a/sdk/eggai/adapters/a2a/plugin.py b/sdk/eggai/adapters/a2a/plugin.py index 287a7c7e..7607ec5a 100644 --- a/sdk/eggai/adapters/a2a/plugin.py +++ b/sdk/eggai/adapters/a2a/plugin.py @@ -22,7 +22,7 @@ def __init__(self): self.config = None self.skills: dict[str, Any] = {} # AgentSkill objects self.handlers: dict[str, Callable] = {} # Handler functions - self.data_types: dict[str, type] = {} # Data types for conversion + self.data_types: dict[str, type | None] = {} # Data types for conversion def init( self, diff --git a/sdk/eggai/agent.py b/sdk/eggai/agent.py index 8837cfda..dbb25579 100644 --- a/sdk/eggai/agent.py +++ b/sdk/eggai/agent.py @@ -9,7 +9,7 @@ from .transport import get_default_transport from .transport.base import Transport -HANDLERS_IDS = defaultdict(int) +HANDLERS_IDS: defaultdict[str, int] = defaultdict(int) PLUGIN_LIST = ["a2a"] @@ -43,7 +43,7 @@ def __init__(self, name: str, transport: Transport | None = None, **kwargs): self._stop_registered = False # Initialize plugins system - self.plugins = {} + self.plugins: dict[Any, dict[str, Any]] = {} # Initialize plugins generically for plugin_name in PLUGIN_LIST: @@ -132,13 +132,25 @@ def decorator(handler: Callable[[dict[str, Any]], "asyncio.Future"]): kwargs.pop(key) plugin_found_keys.add(plugin_name) - self._subscriptions.append((channel_name, handler, kwargs)) - - # Call plugin subscribe methods if they have relevant kwargs + # Call plugin subscribe methods if they have relevant kwargs. Validate + # before registering the subscription so a misconfiguration doesn't leave + # a half-registered handler in self._subscriptions. for plugin_found_key in plugin_found_keys: - self.plugins[plugin_found_key]["_instance"].subscribe( - channel_name, handler, **original_kwargs - ) + plugin = self.plugins.get(plugin_found_key) + if plugin is None or "_instance" not in plugin: + # Plugin-prefixed kwargs were passed to subscribe(), but the + # plugin is only instantiated when matching kwargs are passed to + # Agent(...). Without that, self.plugins has no entry and the + # access below would raise an opaque KeyError. + raise ValueError( + f"Subscription passed {plugin_found_key!r}-prefixed kwargs, " + f"but the {plugin_found_key!r} plugin is not initialized on " + f"this agent. Pass {plugin_found_key!r} configuration to the " + "Agent(...) constructor to enable it." + ) + plugin["_instance"].subscribe(channel_name, handler, **original_kwargs) + + self._subscriptions.append((channel_name, handler, kwargs)) return handler diff --git a/sdk/eggai/channel.py b/sdk/eggai/channel.py index 8c4e2714..c518e335 100644 --- a/sdk/eggai/channel.py +++ b/sdk/eggai/channel.py @@ -10,7 +10,7 @@ from .transport import get_default_transport from .transport.base import Transport -HANDLERS_IDS = defaultdict(int) +HANDLERS_IDS: defaultdict[str, int] = defaultdict(int) # Environment variable: EGGAI_NAMESPACE # Purpose: Prefix for all channel names to enable namespace isolation in shared transports. @@ -29,7 +29,7 @@ class Channel: Connection is established lazily on the first publish or subscription. """ - def __init__(self, name: str = None, transport: Transport | None = None): + def __init__(self, name: str | None = None, transport: Transport | None = None): """ Initialize a Channel instance. diff --git a/sdk/eggai/hooks.py b/sdk/eggai/hooks.py index 3b13e016..37aa2f1a 100644 --- a/sdk/eggai/hooks.py +++ b/sdk/eggai/hooks.py @@ -10,7 +10,7 @@ F = TypeVar("F", bound=Callable[..., Coroutine[Any, Any, Any]]) # Global variables for shutdown handling. -_STOP_CALLBACKS = [] +_STOP_CALLBACKS: list[Callable[..., Any]] = [] _EXIT_EVENT = None # will be lazily created _SIGNAL_HANDLERS_INSTALLED = False _CLEANUP_STARTED = False diff --git a/sdk/eggai/tracing.py b/sdk/eggai/tracing.py index f109018f..a7a49c7d 100644 --- a/sdk/eggai/tracing.py +++ b/sdk/eggai/tracing.py @@ -141,6 +141,7 @@ def setup_tracing( if isinstance(trace.get_tracer_provider(), ProxyTracerProvider): trace.set_tracer_provider(provider) else: + # Adopt the already-configured provider (API base type, not the SDK one). provider = trace.get_tracer_provider() from importlib.metadata import version diff --git a/sdk/eggai/transport/base.py b/sdk/eggai/transport/base.py index add0ecbe..e34c34c1 100644 --- a/sdk/eggai/transport/base.py +++ b/sdk/eggai/transport/base.py @@ -37,7 +37,7 @@ async def publish(self, channel: str, message: dict[str, Any] | BaseModel): async def subscribe( self, channel: str, - callback: Callable[[dict[str, Any]], "asyncio.Future"], + handler: Callable[[dict[str, Any]], "asyncio.Future"], **kwargs, ) -> Callable: """ diff --git a/sdk/eggai/transport/defaults.py b/sdk/eggai/transport/defaults.py index 4b77f741..c56f1cf9 100644 --- a/sdk/eggai/transport/defaults.py +++ b/sdk/eggai/transport/defaults.py @@ -30,4 +30,5 @@ def get_default_transport() -> "Transport": "Use eggai_set_default_transport() to set a different default transport." ) eggai_set_default_transport(lambda: InMemoryTransport()) + assert _DEFAULT_TRANSPORT_FACTORY is not None # set just above if it was None return _DEFAULT_TRANSPORT_FACTORY() diff --git a/sdk/eggai/transport/inmemory.py b/sdk/eggai/transport/inmemory.py index 5d8e9ca4..1c7ed00a 100644 --- a/sdk/eggai/transport/inmemory.py +++ b/sdk/eggai/transport/inmemory.py @@ -6,7 +6,8 @@ from collections.abc import Callable from typing import Any -from eggai.schemas import BaseMessage +from pydantic import BaseModel + from eggai.transport import Transport from eggai.transport.middleware_utils import wrap_handler_with_filters @@ -90,7 +91,7 @@ async def disconnect(self): self._consume_tasks.clear() self._connected = False - async def publish(self, channel: str, message: dict[str, Any] | BaseMessage): + async def publish(self, channel: str, message: dict[str, Any] | BaseModel): """ Publishes a message to the given channel. The message is put into all queues for that channel so each consumer group receives it. @@ -111,7 +112,7 @@ async def publish(self, channel: str, message: dict[str, Any] | BaseMessage): async def subscribe( self, channel: str, - callback: Callable[[dict[str, Any]], "asyncio.Future"], + handler: Callable[[dict[str, Any]], "asyncio.Future"], **kwargs, ): """ @@ -129,7 +130,7 @@ async def subscribe( # and typed delivery identical across all transports and is the single place # that rejects invalid filter-option combinations. Tracing stays outermost. final_callback = wrap_handler_with_filters( - callback, + handler, data_type=kwargs.get("data_type"), filter_by_data=kwargs.get("filter_by_data"), filter_by_message=kwargs.get("filter_by_message"), diff --git a/sdk/eggai/transport/kafka.py b/sdk/eggai/transport/kafka.py index 9842ab1f..7a3d4fa4 100644 --- a/sdk/eggai/transport/kafka.py +++ b/sdk/eggai/transport/kafka.py @@ -5,8 +5,8 @@ from aiokafka.admin import AIOKafkaAdminClient, NewTopic from aiokafka.errors import TopicAlreadyExistsError from faststream.kafka import KafkaBroker +from pydantic import BaseModel -from eggai.schemas import BaseMessage from eggai.transport.base import Transport from eggai.transport.middleware_utils import wrap_handler_with_filters @@ -141,7 +141,11 @@ async def _ensure_topic(self, topic: str): and self.broker._producer ): try: - await self.broker._producer._producer.client.force_metadata_update() + # Reaches into aiokafka internals via FastStream's producer — there + # is no public API for force_metadata_update(). Guarded by the + # hasattr check above and a broad except so a FastStream internals + # change degrades gracefully (stale metadata, not a crash). + await self.broker._producer._producer.client.force_metadata_update() # type: ignore[attr-defined] except Exception: pass @@ -151,7 +155,7 @@ async def ensure_topic(self, channel: str): await self._ensure_topic(channel) self._topics_to_create.add(channel) - async def publish(self, channel: str, message: dict[str, Any] | BaseMessage): + async def publish(self, channel: str, message: dict[str, Any] | BaseModel): """ Publishes a message to the specified Kafka topic (channel). diff --git a/sdk/eggai/transport/middleware_utils.py b/sdk/eggai/transport/middleware_utils.py index f565abff..7740f1b7 100644 --- a/sdk/eggai/transport/middleware_utils.py +++ b/sdk/eggai/transport/middleware_utils.py @@ -14,7 +14,8 @@ from collections.abc import Callable from typing import Any -from pydantic import ValidationError +from pydantic import BaseModel, ValidationError +from pydantic_core import PydanticUndefined # Identity attributes copied from the user handler onto a wrapper. We intentionally # do NOT use functools.wraps here: it sets __wrapped__, which inspect.signature @@ -52,7 +53,7 @@ async def _invoke(handler: Callable, arg: Any) -> Any: def wrap_handler_with_filters( handler: Callable, *, - data_type: type | None = None, + data_type: type[BaseModel] | None = None, filter_by_data: Callable[[Any], bool] | None = None, filter_by_message: Callable[[dict[str, Any]], bool] | None = None, ) -> Callable: @@ -98,6 +99,16 @@ def wrap_handler_with_filters( "(the discriminator used to match messages, as on BaseMessage)." ) expected_type = data_type.model_fields["type"].default + if expected_type is PydanticUndefined: + # The 'type' field exists but has no default, so there is no + # discriminator value to match against — every message would fail the + # check at line below and be silently dropped. Fail loudly instead. + raise ValueError( + f"data_type {data_type.__name__!r} has a 'type' field with no " + "default, so there is no value to match messages against (every " + "message would be silently dropped). Give it a default discriminator, " + 'e.g. `type: str = "OrderCreated"`.' + ) async def typed_handler(message: dict[str, Any]) -> Any: try: @@ -105,7 +116,7 @@ async def typed_handler(message: dict[str, Any]) -> Any: except (ValidationError, ValueError, TypeError): # Wrong shape / payload for this data_type — not ours to handle. return None - if typed_message.type != expected_type: + if getattr(typed_message, "type", None) != expected_type: return None if filter_by_data is not None and not filter_by_data(typed_message): return None diff --git a/sdk/eggai/transport/pending_reclaimer.py b/sdk/eggai/transport/pending_reclaimer.py index 92772a12..db622ca9 100644 --- a/sdk/eggai/transport/pending_reclaimer.py +++ b/sdk/eggai/transport/pending_reclaimer.py @@ -153,6 +153,16 @@ def __init__(self, redis_url: str, connection_kwargs: dict[str, Any] | None = No self._tasks: dict[tuple[str, str, str], asyncio.Task] = {} self._running = False + @property + def _client(self) -> aioredis.Redis: + """The connected Redis client; raises if accessed before start().""" + client = self._redis_client + if client is None: + raise RuntimeError( + "PendingReclaimerManager used before start(); no Redis client." + ) + return client + def add(self, config: ReclaimerConfig) -> tuple[str, str, str]: key = (config.stream, config.group, config.consumer) self._configs[key] = config @@ -187,7 +197,7 @@ async def stop(self) -> None: await asyncio.gather(*self._tasks.values(), return_exceptions=True) self._tasks.clear() if self._redis_client is not None: - await self._redis_client.aclose() + await self._client.aclose() self._redis_client = None async def _run(self, config: ReclaimerConfig) -> None: @@ -223,9 +233,9 @@ async def _ensure_group(self, config: ReclaimerConfig) -> None: MKSTREAM when the stream itself is gone. """ try: - stream_exists = await self._redis_client.exists(config.stream) + stream_exists = await self._client.exists(config.stream) create_id = "0" if stream_exists else "$" - await self._redis_client.xgroup_create( + await self._client.xgroup_create( name=config.stream, groupname=config.group, id=create_id, @@ -248,11 +258,9 @@ async def _xadd(self, stream: str, fields: dict, max_len: int | None) -> None: recommended production setting. ``max_len=None`` means no trimming. """ if max_len is not None: - await self._redis_client.xadd( - stream, fields, maxlen=max_len, approximate=True - ) + await self._client.xadd(stream, fields, maxlen=max_len, approximate=True) else: - await self._redis_client.xadd(stream, fields) + await self._client.xadd(stream, fields) def _effective_idle_ms(self, config: ReclaimerConfig, retry_count: int) -> float: """Idle time a message must accrue before this reclaim cycle treats it as stale. @@ -307,7 +315,7 @@ async def _read_retry_count(self, stream: str, msg_id: Any) -> int: forever. """ try: - entries = await self._redis_client.xrange(stream, min=msg_id, max=msg_id) + entries: Any = await self._client.xrange(stream, min=msg_id, max=msg_id) if not entries: return 0 _id, fields = entries[0] @@ -331,7 +339,7 @@ async def _reclaim_once(self, config: ReclaimerConfig) -> None: candidates: list[tuple[Any, int]] = [] # (message_id, idle_ms) cursor = "-" while True: - page: list[dict] = await self._redis_client.xpending_range( + page: list[dict] = await self._client.xpending_range( name=config.stream, groupname=config.group, min=cursor, @@ -369,7 +377,7 @@ async def _reclaim_once(self, config: ReclaimerConfig) -> None: if not stale_ids: return - claimed: list[tuple[Any, dict]] = await self._redis_client.xclaim( + claimed: Any = await self._client.xclaim( name=config.stream, groupname=config.group, consumername=config.consumer, # "-reclaimer" suffix — no feedback loop @@ -399,7 +407,7 @@ async def _reclaim_once(self, config: ReclaimerConfig) -> None: if not parsed_ok: if config.dlq_stream is not None: await self._xadd(config.dlq_stream, fields, config.max_len) - await self._redis_client.xack(config.stream, config.group, msg_id) + await self._client.xack(config.stream, config.group, msg_id) logger.warning( "Message %s has an unparseable envelope; moved to DLQ %s " "(retry count cannot be tracked)", @@ -410,7 +418,7 @@ async def _reclaim_once(self, config: ReclaimerConfig) -> None: config, fields, data_key, msg_id_str, new_count ) else: - await self._redis_client.xack(config.stream, config.group, msg_id) + await self._client.xack(config.stream, config.group, msg_id) logger.error( "Message %s has an unparseable envelope and no DLQ is " "configured; dropping it to avoid a retry-stream livelock", @@ -425,7 +433,7 @@ async def _reclaim_once(self, config: ReclaimerConfig) -> None: and new_count > config.max_retries ): await self._xadd(config.dlq_stream, fields, config.max_len) - await self._redis_client.xack(config.stream, config.group, msg_id) + await self._client.xack(config.stream, config.group, msg_id) logger.warning( "Message %s exceeded max_retries=%d; moved to DLQ %s", msg_id_str, @@ -437,7 +445,7 @@ async def _reclaim_once(self, config: ReclaimerConfig) -> None: ) else: await self._xadd(config.retry_stream, fields, config.max_len) - await self._redis_client.xack(config.stream, config.group, msg_id) + await self._client.xack(config.stream, config.group, msg_id) logger.debug("Reclaimed %s → %s", msg_id_str, config.retry_stream) async def _invoke_on_dlq( diff --git a/sdk/eggai/transport/redis.py b/sdk/eggai/transport/redis.py index c75d2749..009eb6b6 100644 --- a/sdk/eggai/transport/redis.py +++ b/sdk/eggai/transport/redis.py @@ -9,9 +9,9 @@ import redis.asyncio as aioredis from faststream import AckPolicy from faststream.redis import RedisBroker, StreamSub +from pydantic import BaseModel from redis.exceptions import ResponseError -from eggai.schemas import BaseMessage from eggai.transport.base import Transport from eggai.transport.middleware_utils import wrap_handler_with_filters from eggai.transport.pending_reclaimer import PendingReclaimerManager, ReclaimerConfig @@ -235,7 +235,7 @@ async def disconnect(self): await self._reclaimer_manager.stop() await self.broker.stop() - async def publish(self, channel: str, message: dict[str, Any] | BaseMessage): + async def publish(self, channel: str, message: dict[str, Any] | BaseModel): """ Publishes a message to the specified Redis stream. diff --git a/sdk/poetry.lock b/sdk/poetry.lock index 85b8f449..d54d9635 100644 --- a/sdk/poetry.lock +++ b/sdk/poetry.lock @@ -1662,6 +1662,154 @@ enabler = ["pytest-enabler (>=3.4)"] test = ["pyfakefs", "pytest (>=6,!=8.1.*)"] type = ["pygobject-stubs", "pytest-mypy (>=1.0.1)", "shtab", "types-pywin32"] +[[package]] +name = "librt" +version = "0.15.0" +description = "Mypyc runtime library" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +markers = "platform_python_implementation != \"PyPy\"" +files = [ + {file = "librt-0.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e1a49adf16a7c9d9646816c2946135527197b6fcf4347c7b8b761cf1bfbf4489"}, + {file = "librt-0.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:81a398f45b45a59200e13cd5ad1ae1d3f44334de98b148331afe2cdfee701c52"}, + {file = "librt-0.15.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4eafbaff06b9563f8b1c850621ce51605de05208e09d4d71ce490bc972b7b9e8"}, + {file = "librt-0.15.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:b0411b4066db926b80258c60dcb0e6db4c9cee312eab45b7e8866b17ddf9ada1"}, + {file = "librt-0.15.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:febb1ce6cac545a54e6b769982824e955a700fdd9fbf3a08a3d82c990968b57d"}, + {file = "librt-0.15.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b230acc1c3bfe2d6f2627ba2b95dc92e58aa494600e9722d0e6ccbc931e59702"}, + {file = "librt-0.15.0-cp310-cp310-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6da110e5f314c19ab8478464d02ae18808ae73d522c15260fa4918acdcd64da9"}, + {file = "librt-0.15.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:eab9208b00ca55bf75983ec99f7bf13acc746a36102e98953addaad7f7ea1e1b"}, + {file = "librt-0.15.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:6c013cd3a1721e69e14380ada97eaa4b7b0cdf1c6b96fa765d4ea47c875088db"}, + {file = "librt-0.15.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:567b1c430f8bd560e689421468278ac5941bab4a05303b5d95b6ae10db03f451"}, + {file = "librt-0.15.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:29c4cab9df457b19672c39be7f384ebb2bc925c4e2684b8780c222b43eb36389"}, + {file = "librt-0.15.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bccbd8e5b0bffb7106cf18eb1baa3d7194b1cebb3b4b1cdbd4bdb19382a6ee6c"}, + {file = "librt-0.15.0-cp310-cp310-win32.whl", hash = "sha256:8ae493ed5f659a7761c43d42f183db514536073ded9bcf671d2d1df47e29a07e"}, + {file = "librt-0.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:bc25fb356d0c7810bb49ff3df908ad1fda6995d660ab099ded69244ed7ab6053"}, + {file = "librt-0.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:823b92cf3c18ecd08afc70c42473888b41b6e8ef5046f3b82c05c154a2fa3d22"}, + {file = "librt-0.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c70bc1b602cf59917e8f0c7a2cbc8bcc6fbc14d5486136b00707a79619121d63"}, + {file = "librt-0.15.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:814ff83a25b5fce8b9c80c4dd803153fb5c5599fc74db9e022466938368957ef"}, + {file = "librt-0.15.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:57f5eeb6ad4c180de583b1038e61fe5fbd9796bb69a8a1c1a0c7ddbec4c8c60f"}, + {file = "librt-0.15.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:82909c8f7eb9952656b65d3147afde4cf8e6d5a991eebc86418b5e65843b0ab8"}, + {file = "librt-0.15.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f779070399f991400fc451719e0ea388eb7de313388bada2c127a35de05f798a"}, + {file = "librt-0.15.0-cp311-cp311-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bac89069bc496ebdf4f79ebb57bbd10d0b214c8454225deb672d91002bd17e18"}, + {file = "librt-0.15.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e0d00c708fb2f5822b152429b1ac80a58dbbbc3f6c232c4d13a3f7fcf2ea5b4c"}, + {file = "librt-0.15.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6c6624fe268625869485553dd7cc1daf30d22558215bb2a4ff16f67a9801a31a"}, + {file = "librt-0.15.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f56b397858a23dacf35ede366ed2212fdc03a6a57a1ad36468ad6e9dc5fac091"}, + {file = "librt-0.15.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:4388184646efe2054911c5b00a1077d6d1ee86a95b7e8ba96dc7850a809f3f40"}, + {file = "librt-0.15.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:97335f59082f9fe2ce6c2a9cc6433a0114bbb6cd4d5c09dd76c95c68b9f9a8b0"}, + {file = "librt-0.15.0-cp311-cp311-win32.whl", hash = "sha256:83380ffde38062a2e9bb55d83e74474f6614665528b98a6928720fc006dfffbb"}, + {file = "librt-0.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:f75720477ee05d509a310e856cacc8d909adc182f7b91193c207bcc26d7ee6db"}, + {file = "librt-0.15.0-cp311-cp311-win_arm64.whl", hash = "sha256:256237037a3ab001ae8d9803b2d43562a4c3aa38739843694349e4d5ebb0fd56"}, + {file = "librt-0.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e87bc679f86a99aa3b26e3c78eeb821a247c9a28eae48eaafcc32c3bf4c3bb9e"}, + {file = "librt-0.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:71599e011ac880e8e45d46047d714871894c7d4ab6f25626f8d4f89da21f368d"}, + {file = "librt-0.15.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c802434092b769b1d613ed2e13fac15fbfce1934a74bd10283b03c0fae231cd1"}, + {file = "librt-0.15.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5500eeae393a184d14e1f35645962c27129d20c81afa4069e6ef826ebc2b3aaa"}, + {file = "librt-0.15.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6ecfc32dfb46fb7b565bcd6abf9412acf978775a998273d22888a6d7953730dd"}, + {file = "librt-0.15.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:89cc46cfd15022e35084355478c9ac809d90b1152222706ac9a7655ec21df6fa"}, + {file = "librt-0.15.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d5f51401d102c885b9ca509e62c79b1dbff286e1b9b047fde6f763780789356d"}, + {file = "librt-0.15.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:cc30523e3f1a23fb7511cc659834a0d01a1042bb9de359bc1c131cc4ec6c9656"}, + {file = "librt-0.15.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:59fe030d8ae4a57e3fb7756bf35a858de74e04066fc8555c53d0af979132af81"}, + {file = "librt-0.15.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5a6526a2a956bbb1e4ae3568c82e650fc99119c66bb011ea60715744955a2b4d"}, + {file = "librt-0.15.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:85ea21ec6730194d67156b0e0b5430ccb1d61f8b8b907e39b37f9812b74a13f0"}, + {file = "librt-0.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1e47b8ba865d7ede071a91a7163073bbaeb72541f1ef8a07d512c45c7b5007f2"}, + {file = "librt-0.15.0-cp312-cp312-win32.whl", hash = "sha256:a5207ec414d1c4a2a7231b2086970dc036f94293cdf338190984958a013a42f1"}, + {file = "librt-0.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:73b30cfa976659b3917c8f6153bdb0591c6a9ec6583599fd24a689b690622022"}, + {file = "librt-0.15.0-cp312-cp312-win_arm64.whl", hash = "sha256:a54cf9e0ef47b96af580849db5471142200568ce1e02cbf416addab551369570"}, + {file = "librt-0.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:db13ca398005abcbe538deda87b686d9bd08b7001cf40c4c06b444960ae10a26"}, + {file = "librt-0.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa1f1995789dca3698bc550aaceb09a51bd5df0a057ff84ff15296cd1975b801"}, + {file = "librt-0.15.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55456ea87d8df21808446d03817be2f65e20391c1c615d9187440dff28cd08dc"}, + {file = "librt-0.15.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5a86a5a08c2235316bdb359d5dbb6ce0abfca7fac06363103e2c5af571d92f95"}, + {file = "librt-0.15.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e56b6a368529bed262da40ce13f8fef590db0479819cca84f16a1f01ac356d0b"}, + {file = "librt-0.15.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:234d8d394721fa0d786af15ebf1f3fb7f3ed82fd1cd0cde45c2f247b5d4281d2"}, + {file = "librt-0.15.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d8363d7accb0286ac3a0e633f396e93800dafb8150494505daf9515bbda591f3"}, + {file = "librt-0.15.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0f0ee3644d951f31055ad07d77d92520e84505dd7a432cc4cd501dd70ee06785"}, + {file = "librt-0.15.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2cfd1a81a648806e6a7717be4cc4d1bb392fa229752bf8444ba365e381e984d6"}, + {file = "librt-0.15.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:a6cd22c9da0d866558e46a041f1cc0c2bbb26b61b137b2347fa834c332e1d101"}, + {file = "librt-0.15.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:6d5225ef8801e4ea5e482fa9b5dfb891dd9ef6f6d870f1f25d449ca2c70ac218"}, + {file = "librt-0.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d28a05796b99f749bf8794f17ba9ba1612d0076b802e9cfc62c554634e9ce3b"}, + {file = "librt-0.15.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:2067ff438048cead9d223ca5675bae2a25e520a7c3e6c1498bf9c6892d22caab"}, + {file = "librt-0.15.0-cp313-cp313-win32.whl", hash = "sha256:1cd3b721f24c206398b9e26da3c3a9c011e6e89d06f318ba8ebefc30f1003890"}, + {file = "librt-0.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:f395a4a9a03ac062dbe9a9f82e0c720502e590a38feee6a757bc82e9c63afbd8"}, + {file = "librt-0.15.0-cp313-cp313-win_arm64.whl", hash = "sha256:0a15cb554761247d84a3ec0cbdf4078d70725384f0e4662c0fa3b26266eb60ad"}, + {file = "librt-0.15.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f5de7feedc56337a088eb15cd9fafa9938367362221d8cc62c642b7f94821993"}, + {file = "librt-0.15.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6c0eb900c0e91f4aebe680845242e614f1864edfd44106380d0752ac29522bf8"}, + {file = "librt-0.15.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e8c9a650a188e38bac005048cbe6342e81407782944d01934540ab75e417df21"}, + {file = "librt-0.15.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:92bfed8deec93df30286b9fe9e3b1dd17329cc076a192b4ee5ec223841d54953"}, + {file = "librt-0.15.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ec4b19788f835711a2072f9dbe6b03b3bf32ed1f0fb30cf399bdd59d9f0c33fa"}, + {file = "librt-0.15.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d4c7bacb70930f3d0a56f4ecf1be474a1f0d941b01dd73b756f3c256d42cb879"}, + {file = "librt-0.15.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3e79f05e4a08b4d880342673312bbc895b56df7765605796f15902eb5367d3ae"}, + {file = "librt-0.15.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a417149c0cba4d50b61e992e5a15e69eaf96746609b461cc4ed168aeef6b79dd"}, + {file = "librt-0.15.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:da7a94d6a3411f579d72aa3e3bc5fbca7ed4549f3dbd7e5de3aa567333374285"}, + {file = "librt-0.15.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:856f743ae607f2c1380eccb566c0038a9fb3eabf0fc2be2704d76d9f73557239"}, + {file = "librt-0.15.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:779a6e7c894737e5983e7790a9c78c4000c30e23c9aada08081bdbea53b0fa60"}, + {file = "librt-0.15.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:96bb17dbe8bab3c0954fbebfc69ed395599de75b6bbc35e3270a878e15d4dd65"}, + {file = "librt-0.15.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:7220697efaa6e5348fc3d18ee7f8563d4bfecd9872b37ffb915bfc1d08840622"}, + {file = "librt-0.15.0-cp314-cp314-win32.whl", hash = "sha256:f54598964d357b1c5ab77cf5d92f21e598fe0e23cdbe9618480807f81b4eba15"}, + {file = "librt-0.15.0-cp314-cp314-win_amd64.whl", hash = "sha256:3ff5893a2c23d886aa9ce786de5ac6ddc74aeeaf90743682b74d920e117d2e28"}, + {file = "librt-0.15.0-cp314-cp314-win_arm64.whl", hash = "sha256:3722a099730704c9a3d70c879fc0f51daec25fe5f1555672d97bc595abeafb95"}, + {file = "librt-0.15.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:38c0c7d4b6fc06c3324b3f9162c8391bfc4fd9dde53afe1033ce7edb48d5a714"}, + {file = "librt-0.15.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8b2fdd7ead3c995c37940a790690660d0ca006c302db26cc51933f6766866fc3"}, + {file = "librt-0.15.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2fde98cf1fc4bac144ce23c2c4c017b924ba714509ea9334977b0b27050c837d"}, + {file = "librt-0.15.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:e3b461183c5fa7681b48560f91515f53a953122fb30c71e07abc67d7ddf58c38"}, + {file = "librt-0.15.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4bbcc257e3babea20a91715c361b24554ec4e8f51aa578568afc230799fe1a19"}, + {file = "librt-0.15.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b845b8d48088fad0cadc84be4b8fda63203be7e9237b71015b3925443c1f35ab"}, + {file = "librt-0.15.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b30e600e8f337b9bd7f39b86d9fdfedc73cc46e3d0f745931a23a234220bb7e2"}, + {file = "librt-0.15.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:64b0c8c35aa4c4ed79896359f3e0b285cbe4e610042106500da4811c322cc108"}, + {file = "librt-0.15.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0da0d94cb802f32a0524653e7201f2cef72d5f700a5407678f5290483d4fcd08"}, + {file = "librt-0.15.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:4a6369168d371207339b1e50d4532b06a7121586141f82599505a3f315751d47"}, + {file = "librt-0.15.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c434e072557ade9cbc642d052c89d031efe47d5c9614523619d0d74a02378e81"}, + {file = "librt-0.15.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c7eec6a42018bc1d45763b1c162d3d2bf7c3b9a1b0ed30d3e91dcba390efefcc"}, + {file = "librt-0.15.0-cp314-cp314t-win32.whl", hash = "sha256:6912fa5e635d74529ac7cdb1bdf6ca3af4453da8d1edbe0110ee1cb4ad407ebf"}, + {file = "librt-0.15.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8e11699ed745931c395acd3621b07062e0f840efa6935aad87a64ed0995f0915"}, + {file = "librt-0.15.0-cp314-cp314t-win_arm64.whl", hash = "sha256:5d2a91724463bfed4f573cd7a9fdc856d2e230d0c0e5a61416a93481dccd8605"}, + {file = "librt-0.15.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:8443e38dcfcfdbcf5add5118c623efd788d65ac2e25756d6251a54a06a4d0aca"}, + {file = "librt-0.15.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:6d15a29033c57490cfe2069097c6fc4049e4e65ffbb749be7dc453b7c4c68965"}, + {file = "librt-0.15.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d2c05c729b589e734c09578bf5964be48a911765484840d017bbc84f49d4c4ad"}, + {file = "librt-0.15.0-cp315-cp315-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:fa60887537e1d0cd2d9982269d33a709bf54b195cd2b9364fc0a758022af5bd9"}, + {file = "librt-0.15.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d8bc24219b24c0af375718942ab75e3544b2763085f40f965be4326734ae8328"}, + {file = "librt-0.15.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:86a21a7bd3fe3a419512ef424cc1c020f6771d0b29cfddff36d1635a855e63f0"}, + {file = "librt-0.15.0-cp315-cp315-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dbab647e88d90b3167b91efe7091e248653688ed4337e4f90907a722c7361bb9"}, + {file = "librt-0.15.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:d8edcf6f550e918dca779c069b9e156385c60b406f99fc7641f32c52f7193659"}, + {file = "librt-0.15.0-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:8b62076030baa2d8b1501a46bf0e19c27a489aa90671c55665bff7887f7660b0"}, + {file = "librt-0.15.0-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:d00d20d1818e82a07a0ee0aa89a98b17ed7916b92441090b683719cb20a59b6d"}, + {file = "librt-0.15.0-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:4e6ee93fc3cf848dcbf0cce2eca73d8e7dcd0cc2b6df3a529d57750b30a4c55c"}, + {file = "librt-0.15.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:32896a0af72508ea979e0acb4e4c04cbeeae04938167950d535c83c45597167d"}, + {file = "librt-0.15.0-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:ec3ba415afaf951f6951b1dd16d3c8e4f540065fc382d7e70b823a79567ca374"}, + {file = "librt-0.15.0-cp315-cp315-win32.whl", hash = "sha256:d2813ba2503764f0450680c533d13df7cff9b49df1411062eded5f67db4195b9"}, + {file = "librt-0.15.0-cp315-cp315-win_amd64.whl", hash = "sha256:b87d67e33afaf265262f2a66db578284b88ee2e6fcd224579cb5c15518677ad8"}, + {file = "librt-0.15.0-cp315-cp315-win_arm64.whl", hash = "sha256:713bd7df21170b982e729e46870f31d6b437bd1a9b4648cffb529bd3c2ec5c4b"}, + {file = "librt-0.15.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:3de789c82752730f94782a5ee518baf9c05edf85733aeaf73bb6e518755cdf54"}, + {file = "librt-0.15.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:e0b5deec9a8664eb722c797241970fd4aa1894d25fda36a1ddac0f7407606bd6"}, + {file = "librt-0.15.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5563302a8359bc2295bb7084d1a8ed1519df96afb30eb2aa4e0bff7b54228988"}, + {file = "librt-0.15.0-cp315-cp315t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:22d6263b9d39d7bbb286fa791945646e3218f1be2d693e36fb630f1d0e59cd13"}, + {file = "librt-0.15.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:39ffd14646190c454f0d86e0d256b33f00a87a26ab410e619773b841d0e41416"}, + {file = "librt-0.15.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c47318cd3a61401452de11282242937e3e057c4fd3dbaf601e269d0928a06c0a"}, + {file = "librt-0.15.0-cp315-cp315t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a56a1d4f859a82ca5b99fc4b82c9b027b15e3c455c5cd99e7d0719f27bb20b6c"}, + {file = "librt-0.15.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:077471b3182db4e17c36ae91555f36a4d2c00080b267f749bcad34a478a9a302"}, + {file = "librt-0.15.0-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:411ca4d1b905b860ceba7570dd6717a71dedaddcc4b0f77ece710aa41ee11f8d"}, + {file = "librt-0.15.0-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:1256589e0b0adb31751d685a68bce29d73407ddf4ef05d4188f49d5dcf9566d9"}, + {file = "librt-0.15.0-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:f42b74a53e5f26a0ba0007411a7455b66c67ce4022a39cc1f56fc4efd65bcbab"}, + {file = "librt-0.15.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:291bf73caf78b9e88d6fae9bfd693207ff7d832e2fdbe2cf8e746bc13f5f892b"}, + {file = "librt-0.15.0-cp315-cp315t-win32.whl", hash = "sha256:c16d15ee371643ab48dc8248a3e680ebbeca573a13af2c3dd0c985b142d77162"}, + {file = "librt-0.15.0-cp315-cp315t-win_amd64.whl", hash = "sha256:dbd605739f228912dc49027cb764456b9757750bdc2b6b7773164db7096c6fd1"}, + {file = "librt-0.15.0-cp315-cp315t-win_arm64.whl", hash = "sha256:84d244b00604d17df3fc7736c327892d6bba66181254aa4087be807b6c342bdc"}, + {file = "librt-0.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0e2d0c0acf5b0ada7d045912b7cf787c21315c95b38b1fa939ef72d45d366b3d"}, + {file = "librt-0.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f9ca190fe9edc0eb08eec558a509a16d28d91c35667b8f043cba40ed5e77a959"}, + {file = "librt-0.15.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:80811e1c42386ea95c6fb30571d3250ad43d7863f883f787f70517f441150e59"}, + {file = "librt-0.15.0-cp39-cp39-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:88c2a17815c266e6d8180204ff62cb739ab869ada4a746d4c505331526ac58f1"}, + {file = "librt-0.15.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a5fa8f1f916988d0bf1afea005bda37f56ac41a18016e813ccf0097a8d460ca4"}, + {file = "librt-0.15.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:355e3a4c725225a14262004fc1872a552b9d3634b4f791a0dfc80804aafbfd55"}, + {file = "librt-0.15.0-cp39-cp39-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fc1ed11c4ad0b91af24def2050f2840ea4567828e3dd058fbe608d982f6e5465"}, + {file = "librt-0.15.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1f4ef2e71db33df4309167ed7f1520c4fae5e611226e159fa9cf33f93e6ddb3d"}, + {file = "librt-0.15.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1a1a8cd430c7dd0c083f455cb1b328d7fc682b05c31b940906f7845bdff80881"}, + {file = "librt-0.15.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:04d5387b908676c0b8d5d2f5fb58373b4ea382d81f7a6f0fab8ea2a462bb4738"}, + {file = "librt-0.15.0-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:1172c6ad2a88b646e7fe3b480e3fac4ab4418b3443fd8a4061fdd531e0622fc7"}, + {file = "librt-0.15.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:52e8db01f603f5da0ca30987479acff98769382efc8e142fa3962395dcf3ffdb"}, + {file = "librt-0.15.0-cp39-cp39-win32.whl", hash = "sha256:e4c911f15a1652ca94ae9f1abd92e74cbb1b3597d2d92fdd556202f94e8cd455"}, + {file = "librt-0.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:68242379c9b65a582b6e97318a1e9fbd6d445e58954f2d437991c4804ab11578"}, + {file = "librt-0.15.0.tar.gz", hash = "sha256:4e66cbe84437497d951b799d3e1551291b6fb3d643820a7014b3655d57a59162"}, +] + [[package]] name = "markdown-it-py" version = "4.2.0" @@ -1856,6 +2004,90 @@ files = [ ] markers = {main = "extra == \"mcp\"", dev = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\""} +[[package]] +name = "mypy" +version = "1.20.2" +description = "Optional static typing for Python" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "mypy-1.20.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cf5a4db6dca263010e2c7bff081c89383c72d187ba2cf4c44759aac970e2f0c4"}, + {file = "mypy-1.20.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7b0e817b518bff7facd7f85ea05b643ad8bdcce684cf29784987b0a7c8e1f997"}, + {file = "mypy-1.20.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97d7b9a485b40f8ca425460e89bf1da2814625b2da627c0dcc6aa46c92631d14"}, + {file = "mypy-1.20.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1e1c12f6d2db3d78b909b5f77513c11eb7f2dd2782b96a3ab6dffc7d44575c99"}, + {file = "mypy-1.20.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:89dce27e142d25ffbc154c1819383b69f2e9234dc4ed4766f42e0e8cb264ab5c"}, + {file = "mypy-1.20.2-cp310-cp310-win_amd64.whl", hash = "sha256:f376e37f9bf2a946872fc5fd1199c99310748e3c26c7a26683f13f8bdb756cbd"}, + {file = "mypy-1.20.2-cp310-cp310-win_arm64.whl", hash = "sha256:6e2b469efd811707bc530fd1effef0f5d6eebcb7fe376affae69025da4b979a2"}, + {file = "mypy-1.20.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4077797a273e56e8843d001e9dfe4ba10e33323d6ade647ff260e5cd97d9758c"}, + {file = "mypy-1.20.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cdecf62abcc4292500d7858aeae87a1f8f1150f4c4dd08fb0b336ee79b2a6df3"}, + {file = "mypy-1.20.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c566c3a88b6ece59b3d70f65bedef17304f48eb52ff040a6a18214e1917b3254"}, + {file = "mypy-1.20.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0deb80d062b2479f2c87ae568f89845afc71d11bc41b04179e58165fd9f31e98"}, + {file = "mypy-1.20.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bba9ad231e92a3e424b3e56b65aa17704993425bba97e302c832f9466bb85bac"}, + {file = "mypy-1.20.2-cp311-cp311-win_amd64.whl", hash = "sha256:baf593f2765fa3a6b1ef95807dbaa3d25b594f6a52adcc506a6b9cb115e1be67"}, + {file = "mypy-1.20.2-cp311-cp311-win_arm64.whl", hash = "sha256:20175a1c0f49863946ec20b7f63255768058ac4f07d2b9ded6a6b46cfb5a9100"}, + {file = "mypy-1.20.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4dbfcf869f6b0517f70cf0030ba6ea1d6645e132337a7d5204a18d8d5636c02b"}, + {file = "mypy-1.20.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4b6481b228d072315b053210b01ac320e1be243dc17f9e5887ef167f23f5fae4"}, + {file = "mypy-1.20.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:34397cdced6b90b836e38182076049fdb41424322e0b0728c946b0939ebdf9f6"}, + {file = "mypy-1.20.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a5da6976f20cae27059ea8d0c86e7cef3de720e04c4bb9ee18e3690fdb792066"}, + {file = "mypy-1.20.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:56908d7e08318d39f85b1f0c6cfd47b0cac1a130da677630dac0de3e0623e102"}, + {file = "mypy-1.20.2-cp312-cp312-win_amd64.whl", hash = "sha256:d52ad8d78522da1d308789df651ee5379088e77c76cb1994858d40a426b343b9"}, + {file = "mypy-1.20.2-cp312-cp312-win_arm64.whl", hash = "sha256:785b08db19c9f214dc37d65f7c165d19a30fcecb48abfa30f31b01b5acaabb58"}, + {file = "mypy-1.20.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:edfbfca868cdd6bd8d974a60f8a3682f5565d3f5c99b327640cedd24c4264026"}, + {file = "mypy-1.20.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e2877a02380adfcdbc69071a0f74d6e9dbbf593c0dc9d174e1f223ffd5281943"}, + {file = "mypy-1.20.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7488448de6007cd5177c6cea0517ac33b4c0f5ee9b5e9f2be51ce75511a85517"}, + {file = "mypy-1.20.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bb9c2fa06887e21d6a3a868762acb82aec34e2c6fd0174064f27c93ede68ad15"}, + {file = "mypy-1.20.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9d56a78b646f2e3daa865bc70cd5ec5a46c50045801ca8ff17a0c43abc97e3ee"}, + {file = "mypy-1.20.2-cp313-cp313-win_amd64.whl", hash = "sha256:2a4102b03bb7481d9a91a6da8d174740c9c8c4401024684b9ca3b7cc5e49852f"}, + {file = "mypy-1.20.2-cp313-cp313-win_arm64.whl", hash = "sha256:a95a9248b0c6fd933a442c03c3b113c3b61320086b88e2c444676d3fd1ca3330"}, + {file = "mypy-1.20.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:419413398fe250aae057fd2fe50166b61077083c9b82754c341cf4fd73038f30"}, + {file = "mypy-1.20.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e73c07f23009962885c197ccb9b41356a30cc0e5a1d0c2ea8fd8fb1362d7f924"}, + {file = "mypy-1.20.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c64e5973df366b747646fc98da921f9d6eba9716d57d1db94a83c026a08e0fb"}, + {file = "mypy-1.20.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a65aa591af023864fd08a97da9974e919452cfe19cb146c8a5dc692626445dc"}, + {file = "mypy-1.20.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4fef51b01e638974a6e69885687e9bd40c8d1e09a6cd291cca0619625cf1f558"}, + {file = "mypy-1.20.2-cp314-cp314-win_amd64.whl", hash = "sha256:913485a03f1bcf5d279409a9d2b9ed565c151f61c09f29991e5faa14033da4c8"}, + {file = "mypy-1.20.2-cp314-cp314-win_arm64.whl", hash = "sha256:c3bae4f855d965b5453784300c12ffc63a548304ac7f99e55d4dc7c898673aa3"}, + {file = "mypy-1.20.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2de3dcea53babc1c3237a19002bc3d228ce1833278f093b8d619e06e7cc79609"}, + {file = "mypy-1.20.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:52b176444e2e5054dfcbcb8c75b0b719865c96247b37407184bbfca5c353f2c2"}, + {file = "mypy-1.20.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:688c3312e5dadb573a2c69c82af3a298d43ecf9e6d264e0f95df960b5f6ac19c"}, + {file = "mypy-1.20.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29752dbbf8cc53f89f6ac096d363314333045c257c9c75cbd189ca2de0455744"}, + {file = "mypy-1.20.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:803203d2b6ea644982c644895c2f78b28d0e208bba7b27d9b921e0ec5eb207c6"}, + {file = "mypy-1.20.2-cp314-cp314t-win_amd64.whl", hash = "sha256:9bcb8aa397ff0093c824182fd76a935a9ba7ad097fcbef80ae89bf6c1731d8ec"}, + {file = "mypy-1.20.2-cp314-cp314t-win_arm64.whl", hash = "sha256:e061b58443f1736f8a37c48978d7ab581636d6ab03e3d4f99e3fa90463bb9382"}, + {file = "mypy-1.20.2-py3-none-any.whl", hash = "sha256:a94c5a76ab46c5e6257c7972b6c8cff0574201ca7dc05647e33e795d78680563"}, + {file = "mypy-1.20.2.tar.gz", hash = "sha256:e8222c26daaafd9e8626dec58ae36029f82585890589576f769a650dd20fd665"}, +] + +[package.dependencies] +librt = {version = ">=0.8.0", markers = "platform_python_implementation != \"PyPy\""} +mypy_extensions = ">=1.0.0" +pathspec = ">=1.0.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing_extensions = [ + {version = ">=4.6.0", markers = "python_version < \"3.15\""}, + {version = ">=4.14.0", markers = "python_version >= \"3.15\""}, +] + +[package.extras] +dmypy = ["psutil (>=4.0)"] +faster-cache = ["orjson"] +install-types = ["pip"] +mypyc = ["setuptools (>=50)"] +native-parser = ["ast-serialize (>=0.1.1,<1.0.0)"] +reports = ["lxml"] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"}, + {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, +] + [[package]] name = "nh3" version = "0.3.7" @@ -2074,6 +2306,23 @@ files = [ {file = "pathable-0.6.0.tar.gz", hash = "sha256:6404b8b82aef5ff0fd478934137128b99b12212ba35afdde5525ca4f8388ea58"}, ] +[[package]] +name = "pathspec" +version = "1.1.1" +description = "Utility library for gitignore style pattern matching of file paths." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189"}, + {file = "pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a"}, +] + +[package.extras] +hyperscan = ["hyperscan (>=0.7)"] +optional = ["typing-extensions (>=4)"] +re2 = ["google-re2 (>=1.1)"] + [[package]] name = "platformdirs" version = "4.11.7" @@ -3276,7 +3525,6 @@ files = [ {file = "typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8"}, {file = "typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5"}, ] -markers = {dev = "python_version < \"3.13\""} [[package]] name = "typing-inspection" @@ -3776,4 +4024,4 @@ otel = ["opentelemetry-api", "opentelemetry-exporter-otlp-proto-grpc", "opentele [metadata] lock-version = "2.1" python-versions = "^3.10" -content-hash = "251d352c91552996bb9fe841e1b38c6f3d5d3d5a80be242befa0ba57a52bce8b" +content-hash = "182216e3bbc7109c1910ae6eca1ad3941646e3a22a24eed06bcd7e05d411ec7c" diff --git a/sdk/pyproject.toml b/sdk/pyproject.toml index 82bfade8..78ea138e 100644 --- a/sdk/pyproject.toml +++ b/sdk/pyproject.toml @@ -62,6 +62,7 @@ pytest = "^9.0.0" pytest-cov = ">=6,<8" jinja2 = "^3.1.6" ruff = ">=0.14.4,<0.17.0" +mypy = "^1.18" [tool.poetry.scripts] eggai = "cli.main:main" @@ -115,3 +116,11 @@ quote-style = "double" indent-style = "space" skip-magic-trailing-comma = false line-ending = "auto" + +[tool.mypy] +python_version = "3.10" +# The lint job installs no extras, so the a2a adapter and tracing are not type-checked yet. +files = ["eggai"] +ignore_missing_imports = true +warn_unused_ignores = true +warn_redundant_casts = true diff --git a/sdk/tests/test_agent.py b/sdk/tests/test_agent.py new file mode 100644 index 00000000..960dca20 --- /dev/null +++ b/sdk/tests/test_agent.py @@ -0,0 +1,26 @@ +"""Unit tests for Agent subscription wiring. + +Broker-independent: the subscribe decorator runs synchronously at decoration time, +so these need no Redis/Kafka service. +""" + +import pytest + +from eggai import Agent, Channel + + +def test_plugin_kwargs_without_initialized_plugin_raises(): + """Passing plugin-prefixed kwargs (e.g. ``a2a_*``) to subscribe() without + initializing that plugin via the Agent(...) constructor must raise a clear + error rather than an opaque KeyError on self.plugins.""" + agent = Agent("test-agent") # no a2a config -> a2a plugin not initialized + + with pytest.raises(ValueError, match="'a2a' plugin is not initialized"): + + @agent.subscribe(channel=Channel("test"), a2a_skill="greet") + async def handler(message): + return message + + # The guard runs before the subscription is registered, so a rejected + # subscription must not leave a half-registered handler behind. + assert agent._subscriptions == [] diff --git a/sdk/tests/test_middleware_utils.py b/sdk/tests/test_middleware_utils.py index 3736cb52..98fe9d59 100644 --- a/sdk/tests/test_middleware_utils.py +++ b/sdk/tests/test_middleware_utils.py @@ -120,6 +120,18 @@ async def handler(m): wrap_handler_with_filters(handler, data_type=NoType) +def test_data_type_with_no_default_type_is_rejected(): + """A 'type' field with no default has no discriminator value to match, so the + wrapper must raise rather than silently drop every message. Raw BaseMessage + declares `type: str = Field(...)` (required, no default) and triggers this.""" + + async def handler(m): + return m + + with pytest.raises(ValueError, match="'type' field with no .*default"): + wrap_handler_with_filters(handler, data_type=BaseMessage) + + @pytest.mark.asyncio async def test_sync_handler_works_with_filter_by_message(): """A synchronous handler combined with a filter must not raise from an