diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index c10c2ce1..b7c7efc9 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 85091ffe..4d8bd3b5 100644 --- a/sdk/CHANGELOG.md +++ b/sdk/CHANGELOG.md @@ -7,6 +7,38 @@ 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 now type-checked in CI + (`poetry run mypy`). A pragmatic first-adoption config (`ignore_missing_imports`, + `warn_unused_ignores`) lives in `pyproject.toml`; `mypy` is a new dev dependency. + +### Fixed +- **A2A executor**: the JSON-serialisation fallback caught a non-existent + `json.JSONEncodeError`, so when `json.dumps` failed the `except` clause itself + raised `AttributeError` and masked the real error. Now catches `(TypeError, + ValueError)`, the exceptions `json.dumps` actually raises. +- **PEL reclaimer**: data-plane Redis calls now go through a `_client` accessor that + raises a clear error if used before `start()`, instead of an opaque + `AttributeError` on `None`. +- **Typed subscriptions**: a `data_type` whose `type` field has no default (e.g. raw + `BaseMessage`, where `type` is required) silently dropped *every* message, because + the discriminator was compared against `PydanticUndefined`. This now raises a clear + `ValueError` at subscribe time telling you to give `type` a default discriminator. +- **Agent plugins**: passing plugin-prefixed kwargs (e.g. `a2a_*`) to `subscribe()` + without initializing that plugin via the `Agent(...)` constructor raised an opaque + `KeyError`; it now raises a `ValueError` explaining the plugin is not initialized. +- **A2A executor**: the "unknown skill" error path enqueued a raw dict instead of a + proper A2A message; it now uses the same `_send_agent_response` wrapper as every + other response path. +- **A2A executor**: a skill registered with `data_type=None` raised `TypeError` + (`None(...)`) when building the message; the type lookup is now done once and + falls back to the generic `BaseMessage` when no data type is set. + +### Changed +- **Dev dependencies**: bumped `pytest` (9.0.3 → 9.1.0) and `ruff` (0.15.x patch). +- Minor internal type-annotation cleanups across the transports, channel, agent, + and hooks to satisfy mypy (no behaviour change). + ## [0.3.2] - 2026-06-15 ### Fixed 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 44caea77..fac394ec 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 fb35bccd..5180721f 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/schemas.py b/sdk/eggai/schemas.py index 342659fd..3a68cb96 100644 --- a/sdk/eggai/schemas.py +++ b/sdk/eggai/schemas.py @@ -66,7 +66,9 @@ class BaseMessage(BaseModel, Generic[TData]): description="W3C traceparent for distributed trace context propagation.", ) data: TData = Field( - default_factory=dict, + # dict default for the common unparameterised case; concrete subclasses + # bind TData (e.g. to dict) — the factory can't be generic over TData. + default_factory=dict, # type: ignore[assignment] description="Event payload containing application-specific data.", ) diff --git a/sdk/eggai/tracing.py b/sdk/eggai/tracing.py index 3af67198..dddaaeb3 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 053fa0bc..79d9459f 100644 --- a/sdk/eggai/transport/pending_reclaimer.py +++ b/sdk/eggai/transport/pending_reclaimer.py @@ -151,6 +151,16 @@ def __init__(self, redis_url: str, connection_kwargs: dict[str, Any] | None = No self._configs: dict[tuple[str, str, str], ReclaimerConfig] = {} self._tasks: dict[tuple[str, str, str], asyncio.Task] = {} + @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: pass 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: @@ -220,9 +230,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, @@ -245,11 +255,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. @@ -304,7 +312,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 = await self._client.xrange(stream, min=msg_id, max=msg_id) if not entries: return 0 _id, fields = entries[0] @@ -328,7 +336,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, @@ -366,7 +374,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: list[tuple[Any, dict]] = await self._client.xclaim( name=config.stream, groupname=config.group, consumername=config.consumer, # "-reclaimer" suffix — no feedback loop @@ -396,7 +404,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)", @@ -407,7 +415,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", @@ -422,7 +430,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, @@ -434,7 +442,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 7c78e405..f05efbeb 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 @@ -234,7 +234,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 f3356405..51106901 100644 --- a/sdk/poetry.lock +++ b/sdk/poetry.lock @@ -1542,6 +1542,107 @@ 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.11.0" +description = "Mypyc runtime library" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +markers = "platform_python_implementation != \"PyPy\"" +files = [ + {file = "librt-0.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6e94ebfcfa2d5e9926d6c3b9aa4617ffc42a845b4321fb84021b872358c82a0f"}, + {file = "librt-0.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ae627397a2f351560440d872d6f7c8dbb4072e57868e7b2fc5b8b430fe489d45"}, + {file = "librt-0.11.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc329359321b67d24efdf4bc69012b0597001649544db662c001db5a0184794c"}, + {file = "librt-0.11.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:7e82e642ab0f7608ce2fe53d76ca2280a9ee33a1b06556142c7c6fe80a86fc33"}, + {file = "librt-0.11.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:88145c15c67731d54283d135b03244028c750cc9edc334a96a4f5950ebdb2884"}, + {file = "librt-0.11.0-cp310-cp310-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9d36a51b3d93320b686588e27123f4995804dbf1bce81df78c02fc3c6eea9280"}, + {file = "librt-0.11.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d00f3ac06a2a8b246327f11e186a53a100a4d5c7ed52346367e5ec751d51586c"}, + {file = "librt-0.11.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:461bbceede621f1ffb8839755f8663e886087ee7af16294cab7fb4d782c62eeb"}, + {file = "librt-0.11.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:0cad8a4d6a8ff03c9b76f9414caccd78e7cfbc8a2e12fa334d8e1d9932753783"}, + {file = "librt-0.11.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f37aa505b3cf60701562eddb32df74b12a9e380c207fd8b06dd157a943ac7ea0"}, + {file = "librt-0.11.0-cp310-cp310-win32.whl", hash = "sha256:94663a21534637f0e787ec2a2a756022df6e5b7b2335a5cdd7d8e33d68a2af89"}, + {file = "librt-0.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:dec7db73758c2b54953fd8b7fe348c45188fe26b39ee18446196edd08453a5d4"}, + {file = "librt-0.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:93d95bd45b7d58343d8b90d904450a545144eec19a002511163426f8ab1fae29"}, + {file = "librt-0.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ee278c769a713638cdacd4c0436d72156e75df3ebc0166ab2b9dc43acc386c9"}, + {file = "librt-0.11.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f230cb1cbc9faaa616f9a678f530ebcf186e414b6bcbd88b960e4ba1b92428d5"}, + {file = "librt-0.11.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5d63c855d86938d9de93e265c9bd8c705b51ec494de5738340ee93767a686e4b"}, + {file = "librt-0.11.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:993f028be9e96a08d31df3479ac80d99be374d17f3b78e4796b3fd3c913d4e89"}, + {file = "librt-0.11.0-cp311-cp311-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:258d73a0aa66a055e65b2e4d1b8cdb23b9d132c5bb915d9547d804fcaed116cc"}, + {file = "librt-0.11.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0827efe7854718f04aaddf6496e96960a956e676fe1d0f04eb41511fd8ad06d5"}, + {file = "librt-0.11.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7753e57d6e12d019c0d8786f1c09c709f4c3fcc57c3887b24e36e6c06ec938b7"}, + {file = "librt-0.11.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:11bd19822431cc21af9f27374e7ae2e58103c7d98bda823536a6c47f6bb2bb3d"}, + {file = "librt-0.11.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:22bdf239b219d3993761a148ffa134b19e52e9989c84f845d5d7b71d70a17412"}, + {file = "librt-0.11.0-cp311-cp311-win32.whl", hash = "sha256:46c60b61e308eb535fbd6fa622b1ee1bb2815691c1ad9c98bf7b84952ec3bc8d"}, + {file = "librt-0.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:902e546ff044f579ff1c953ff5fce97b636fe9e3943996b2177710c6ef076f73"}, + {file = "librt-0.11.0-cp311-cp311-win_arm64.whl", hash = "sha256:65ac3bc20f78aa0ee5ae84baa68917f89fef4af63e941084dd019a0d0e749f0c"}, + {file = "librt-0.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b87504f1690a23b9a2cca841191a04f83895d4fc2dd04df91d82b1a04ca2ad46"}, + {file = "librt-0.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40071fc5fe0ce8daa6de616702314a01e1250711682b0523d6ab8d4525910cb3"}, + {file = "librt-0.11.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:137e79445c896a0ea7b265f52d23954e05b64222ee1af69e2cb34219067cbb67"}, + {file = "librt-0.11.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:cca6644054e78746d8d4ef238681f9c34ff8b584fe6b988ecebb8db3b15e622a"}, + {file = "librt-0.11.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5b0eea49f5562861ee8d757a32ef7d559c1d35be2aaaa1ec28941d74c9ffc8a"}, + {file = "librt-0.11.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0d1029d7e1ae1a7e647ed6fb5df8c4ce2dffefb7a9f5fd1376a4554d96dac09f"}, + {file = "librt-0.11.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bc3ce6b33c5828d9e80592011a5c584cb2ce86edbc4088405f70da47dc1d1b3b"}, + {file = "librt-0.11.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:936c5995f3514a42111f20099397d8177c79b4d7e70961e396c6f5a0a3566766"}, + {file = "librt-0.11.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:9bc0ca6ad9381cbe8e4aa6e5726e4c80c78115a6e9723c599ed1d73e092bc49d"}, + {file = "librt-0.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:070aa8c26c0a74774317a72df8851facc7f0f012a5b406557ac56992d92e1ec8"}, + {file = "librt-0.11.0-cp312-cp312-win32.whl", hash = "sha256:6bf14feb84b05ae945277395451998c89c54d0def4070eb5c08de544930b245a"}, + {file = "librt-0.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:75672f0bc524ede266287d532d7923dbce94c7514ad07627bac3d0c6d92cc4d9"}, + {file = "librt-0.11.0-cp312-cp312-win_arm64.whl", hash = "sha256:2f10cf143e4a9bb0f4f5af568a00df94a2d69ef41c2579584454bb0fe5cc642c"}, + {file = "librt-0.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:78dc31f7fdfe9c9d0eb0e8f42d139db230e826415bbcabd9f0e9faaaee909894"}, + {file = "librt-0.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fa475675db22290c3158e1d42326d0f5a65f04f44a0e68c3630a25b53560fb9c"}, + {file = "librt-0.11.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:621db29691044bdeda22e789e482e1b0f3a985d90e3426c9c6d17606416205ea"}, + {file = "librt-0.11.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:a9010e2ed5b3a9e158c5fd966b3ab7e834bb3d3aacc8f66c91dd4b57a3799230"}, + {file = "librt-0.11.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c39513d8b7477a2e1ed8c43fc21c524e8d5a0f8d4e8b7b074dbdbe7820a08e2"}, + {file = "librt-0.11.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7aef3cf1d5af86e770ab04bfd993dfc4ae8b8c17f66fb77dd4a7d50de7bbb1a3"}, + {file = "librt-0.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:557183ddc36babe46b27dd60facbd5adb4492181a5be887587d57cda6e092f21"}, + {file = "librt-0.11.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:83d3e1f72bd42f6c5c0b7daec530c3f829bd02db42c70b8ddf0c2d90a2459930"}, + {file = "librt-0.11.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:4ce1f21fbe589bc1afd7872dece84fb0e1144f794a288e58a10d2c54a55c43be"}, + {file = "librt-0.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b09f7044ea2b64c9da42fd3d335666518cfd1c6e8a182c95da73d0214b41e"}, + {file = "librt-0.11.0-cp313-cp313-win32.whl", hash = "sha256:78fddc31cd4d3caa897ad5d31f856b1faadc9474021ad6cb182b9018793e254e"}, + {file = "librt-0.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:8ca8aa88751a775870b764e93bad5135385f563cb8dcee399abf034ea4d3cb47"}, + {file = "librt-0.11.0-cp313-cp313-win_arm64.whl", hash = "sha256:96f044bb325fd9cf1a723015638c219e9143f0dfbc0ca54c565df2b7fc748b44"}, + {file = "librt-0.11.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4a017a95e5837dc15a8c5661d60e05daa96b90908b1aa6b7acdf443cd25c8ebd"}, + {file = "librt-0.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b1ecbd9819deccc39b7542bf4d2a740d8a620694d39989e58661d3763458f8d4"}, + {file = "librt-0.11.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7da327dacd7be8f8ec36547373550744a3cc0e536d54665cd83f8bcd961200e8"}, + {file = "librt-0.11.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:0dc56b1f8d06e60db362cc3fdae206681817f86ce4725d34511473487f12a34b"}, + {file = "librt-0.11.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05fb8fb2ab90e21c8d12ea240d744ad514da9baf381ebfa70d91d20d21713175"}, + {file = "librt-0.11.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cae74872be221df4374d10fec61f93ed1513b9546ea84f2c0bf73ab3e9bd0b03"}, + {file = "librt-0.11.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:32bcc918c0148eb7e3d57385125bac7e5f9e4359d05f07448b09f6f778c2f31c"}, + {file = "librt-0.11.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f9743fc99135d5f78d2454435615f6dec0473ca507c26ce9d92b10b562a280d3"}, + {file = "librt-0.11.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5ba067f4aadae8fda802d91d2124c90c42195ff32d9161d3549e6d05cfe26f96"}, + {file = "librt-0.11.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:de3bf945454d032f9e390b85c4072e0a0570bf825421c8be0e71209fa65e1abe"}, + {file = "librt-0.11.0-cp314-cp314-win32.whl", hash = "sha256:d2277a05f6dcb9fd13db9566aac4fabd68c3ea1ea46ee5567d4eef8efa495a2f"}, + {file = "librt-0.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:ab73e8db5e3f564d812c1f5c3a175930a5f9bc96ccb5e3b22a34d7858b401cf7"}, + {file = "librt-0.11.0-cp314-cp314-win_arm64.whl", hash = "sha256:aea3caa317752e3a466fa8af45d91ee0ea8c7fdd96e42b0a8dd9b76a7931eba1"}, + {file = "librt-0.11.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d1b36540d7aaf9b9101b3a6f376c8d8e9f7a9aec93ed05918f2c69d493ffef72"}, + {file = "librt-0.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:efbb343ab2ce3540f4ecbe6315d677ed70f37cd9a72b1e58066c918ca83acbaa"}, + {file = "librt-0.11.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa0dd688aab3f7914d3e6e5e3554978e0383312fb8e771d84be008a35b9ee548"}, + {file = "librt-0.11.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:f5fb36b8c6c63fdcbb1d526d94c0d1331610d43f4118cc1beb4efef4f3faacb2"}, + {file = "librt-0.11.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4a9a237d13addb93715b6fee74023d5ee3469b53fce527626c0e088aa585805f"}, + {file = "librt-0.11.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5ddd17bd87b2c56ddd60e546a7984a2e64c4e8eab92fb4cf3830a48ad5469d51"}, + {file = "librt-0.11.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bd43992b4473d42f12ff9e68326079f0696d9d4e6000e8f39a0238d482ba6ee2"}, + {file = "librt-0.11.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:f8e3e8056dd674e279741485e2e512d6e9a751c7455809d0114e6ebf8d781085"}, + {file = "librt-0.11.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c1f708d8ae9c56cf38a903c44297243d2ec83fd82b396b977e0144a3e76217e3"}, + {file = "librt-0.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0add982e0e7b9fc14cf4b33789d5f13f66581889b88c2f58099f6ce8f92617bd"}, + {file = "librt-0.11.0-cp314-cp314t-win32.whl", hash = "sha256:2b481d846ac894c4e8403c5fd0e87c5d11d6499e404b474602508a224ff531c8"}, + {file = "librt-0.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:28edb433edde181112a908c78907af28f964eabc15f4dd16c9d66c834302677c"}, + {file = "librt-0.11.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dee008f20b542e3cd162ba338a7f9ec0f6d23d395f66fe8aeeec3c9d067ea253"}, + {file = "librt-0.11.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6bd72d903911d995ab666dbd1871f8b1e80925a699af8063fbf50053329fb05f"}, + {file = "librt-0.11.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0ef69ac715f3cd8e5cd252cb2aebfa72c015492aacc339d5d7bf8fef3c62c677"}, + {file = "librt-0.11.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:624a40c4a4ad7773315c287276cd024509b2c66ff5904f504bfc08d2c70293ab"}, + {file = "librt-0.11.0-cp39-cp39-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:41dc19fe150b69716c8ece4f76773a9e8813fe3e35e032a58b4d46423fb8d7c0"}, + {file = "librt-0.11.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4e8bd98ea9c47ae90b319a087ab28dac493f1ffbc1ecd1f28fcdbf3b7e1108d1"}, + {file = "librt-0.11.0-cp39-cp39-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:84308fc49423ce6475d1c5d1985cd69a8ca9f0325fc7d5f81bb690a3f3625d4e"}, + {file = "librt-0.11.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:ff0fbaf5f44a21beeb0110f2ab64f45135a9536a834b79c0d1ef018f2786bbfa"}, + {file = "librt-0.11.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:9c028a9442a18e266955d364ce42259136e79a7ba14d773e0d778d5f70cd56f1"}, + {file = "librt-0.11.0-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:9f1692105a02bcf853f355032a5fdc5494358ef83d8fd22d16de375c85cec3f5"}, + {file = "librt-0.11.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7a80a71e1fda83cc752a9141e87aae7fef279538597564d670e9ce513f286192"}, + {file = "librt-0.11.0-cp39-cp39-win32.whl", hash = "sha256:140695816ddf3c86eb972981a26f35efd871c44b0c3aed44c8cd01749386617f"}, + {file = "librt-0.11.0-cp39-cp39-win_amd64.whl", hash = "sha256:92f7ff819c197fc30473190a12c2856f325ac90aabfccbeb2072d28cc2e234e3"}, + {file = "librt-0.11.0.tar.gz", hash = "sha256:075dc3ef4458a278e0195cbf6ac9d38808d9b906c5a6c7f7f79c3888276a3fb1"}, +] + [[package]] name = "markdown-it-py" version = "4.2.0" @@ -1727,6 +1828,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.5" @@ -1945,6 +2130,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.10.0" @@ -2335,14 +2537,14 @@ files = [ [[package]] name = "pytest" -version = "9.0.3" +version = "9.1.0" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9"}, - {file = "pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c"}, + {file = "pytest-9.1.0-py3-none-any.whl", hash = "sha256:8ebb0e7888bdf2bdfc602ec51f8f62d50200af37356c74e503c79a94f5c81f32"}, + {file = "pytest-9.1.0.tar.gz", hash = "sha256:41dd9148c08072446394cefd3d79701701335a9f4cae69ba92e39f6c7f5c061c"}, ] [package.dependencies] @@ -2979,30 +3181,30 @@ files = [ [[package]] name = "ruff" -version = "0.15.15" +version = "0.15.17" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" groups = ["dev"] files = [ - {file = "ruff-0.15.15-py3-none-linux_armv6l.whl", hash = "sha256:cf93e5388f412e1b108b1f8b34a6e036b70fe8aff89393befad96fe48670311b"}, - {file = "ruff-0.15.15-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ac5a646d1f6a7dadd5d50842dae2c1f9862ac887ef5d1b1375e02def791fde6e"}, - {file = "ruff-0.15.15-py3-none-macosx_11_0_arm64.whl", hash = "sha256:77d955a431430c66f72dd94e379ad38a16daea3d25094872ac4edf9e797be530"}, - {file = "ruff-0.15.15-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7614ee79c69788cf6cedd568069ade9cecc22a1ad20494efe8d0c9ebb4b622d4"}, - {file = "ruff-0.15.15-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3cdb1679e06a1f6b47bc384714ae96f6e2fb65ca441eb78c43d2ca554176ce1f"}, - {file = "ruff-0.15.15-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2728b93d7b23a603ea2c0ac6eb73d760bd38ec9de35f35fb41e18f7a3fee7622"}, - {file = "ruff-0.15.15-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be582fcc0db438902c7792b08d6ddf6c9b9e21addaa10092c2c741cfb09e5a45"}, - {file = "ruff-0.15.15-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7aa77465b8ecaf1a27bea098d696f7fed5e1eccbd10b321b682d6de586ae5627"}, - {file = "ruff-0.15.15-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48decfa11d740de4889de623be1463308346312f2409a56e24aa280c86162dc4"}, - {file = "ruff-0.15.15-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a5015088452ca0081387063649ec67f06d3d1d6b8b936a1f836b5e9657ecd48c"}, - {file = "ruff-0.15.15-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f5294aab6356c81600fcdea3a62bb1b924dfd5e91767c12318d3f68f86af57cd"}, - {file = "ruff-0.15.15-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:db5bd4d802415cca656dc1616070b725952d6ae95eb5d4831e49fbd94a38f75f"}, - {file = "ruff-0.15.15-py3-none-musllinux_1_2_i686.whl", hash = "sha256:587a6278ed42059191c1a466e490bd7930fb50bd2e255398bc29616c895a61cb"}, - {file = "ruff-0.15.15-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:df0c1c084f5f4be9812f61518a45c440d3c30d69ce4bf6c5270e66d38338f02a"}, - {file = "ruff-0.15.15-py3-none-win32.whl", hash = "sha256:29428ea79694afbe756d45fd59b36f22b6b020dc0443cf7de0173046236964b9"}, - {file = "ruff-0.15.15-py3-none-win_amd64.whl", hash = "sha256:8df0323902e15e24bc4bf246da830573d3cf3352bd0b9a164eab335d111ff4a4"}, - {file = "ruff-0.15.15-py3-none-win_arm64.whl", hash = "sha256:3c8ceca6792f38196b8f589bc92eccd03eef286602da92e5dc05cc42ef6441b7"}, - {file = "ruff-0.15.15.tar.gz", hash = "sha256:b8dff018130b46d8e5bf0f926ef6b60cf871d6d5ae45fc9334e09632daa741d6"}, + {file = "ruff-0.15.17-py3-none-linux_armv6l.whl", hash = "sha256:d9feddb927fc68bd295f5eebc587a7e42cfaf9b65f60ca4a2386febff575da8f"}, + {file = "ruff-0.15.17-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:25805a226d741c47d274a35ad5c10a7dde175fcddfa511d7cf3da0a21eb3eab7"}, + {file = "ruff-0.15.17-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f6ad73b14c2d18a3bf8ad7cb6974294d7f613a7898604826058e6ac64918ef4d"}, + {file = "ruff-0.15.17-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ba0c1e4f95bcb3869d0d30cbd5917071ef2e28665abfec970cdab0492c713ed"}, + {file = "ruff-0.15.17-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:81647960f10bff57d2e51cadd0c3950fe598400c852863a038720ef5b8cca91e"}, + {file = "ruff-0.15.17-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0e01a84ddbc8c16c23055ba3924476850f1bbc1917cebbb9376665a63e74260d"}, + {file = "ruff-0.15.17-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84fe9f653152f8f294f9f7e03bf3a453d8b4a27f7a59c78c8666167f2b17b96c"}, + {file = "ruff-0.15.17-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c0fe88a7676e7a05b73174d4d4a59cb2ac21ff8263583f87a81a6018475a978"}, + {file = "ruff-0.15.17-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecfc3c7878fff94633ab0348524e093f9ce3243080416dd7d14f8ba400174719"}, + {file = "ruff-0.15.17-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:b8461180b22420b1bdc289909410930761629fddf2a5aaf60fae1ab26cedc4c4"}, + {file = "ruff-0.15.17-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6eccbe50a038b503e7140b441aa9c7fc8c1f36edf23ebef9f4165c2f28f568b7"}, + {file = "ruff-0.15.17-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:382fc0521025f5a8ad447d8bdd523545d0d7646adb718eb1c2dac5065ec27c0f"}, + {file = "ruff-0.15.17-py3-none-musllinux_1_2_i686.whl", hash = "sha256:456d41fcd1b2777ad63f09a6e7121d43f7b688bbc76a800c10f7f8fb1f912c3f"}, + {file = "ruff-0.15.17-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b1a04bcc94ae6194e9db05d16ad31f298a7194bfbcb08258bbe589cee1d587b8"}, + {file = "ruff-0.15.17-py3-none-win32.whl", hash = "sha256:596065960ab1ff593f744220c9fe6580eda00a95003cffa9f4048bb5b1bf0392"}, + {file = "ruff-0.15.17-py3-none-win_amd64.whl", hash = "sha256:6769e5fa1710b179b92e0bfa5a51735b35baea9013dadb06d5f44cbcf9547084"}, + {file = "ruff-0.15.17-py3-none-win_arm64.whl", hash = "sha256:f3be1fbb34bcdfd146240d8fb92a709d4c2c8191348580a3c044ec60fa0b4456"}, + {file = "ruff-0.15.17.tar.gz", hash = "sha256:2ec446937fd16c8c4de2674a209cc5af64d9c6f17d21fbf1151054fa0bcf5219"}, ] [[package]] @@ -3161,7 +3363,6 @@ files = [ {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, ] -markers = {dev = "python_version < \"3.13\""} [[package]] name = "typing-inspection" @@ -3454,4 +3655,4 @@ otel = ["opentelemetry-api", "opentelemetry-exporter-otlp-proto-grpc", "opentele [metadata] lock-version = "2.1" python-versions = "^3.10" -content-hash = "7f6b2b1182836b049e2c861aff7ee4e7d9f175ffec1ad795c5668f41ddf9041c" +content-hash = "25b4f3520b90cac90d08f98811e82f763ef5ade2836b57d332f72d7c25b10dfb" diff --git a/sdk/pyproject.toml b/sdk/pyproject.toml index 1f6611e2..dbba8c89 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.16.0" +mypy = "^1.18" [tool.poetry.scripts] eggai = "cli.main:main" @@ -114,3 +115,14 @@ quote-style = "double" indent-style = "space" skip-magic-trailing-comma = false line-ending = "auto" + +[tool.mypy] +python_version = "3.10" +files = ["eggai"] +# Pragmatic first-adoption config: type-check the eggai package, tolerate +# untyped third-party libs (faststream/aiokafka have partial stubs), and flag +# stale `# type: ignore`s so they don't accumulate. Tighten over time +# (check_untyped_defs, then cli/tests) as annotations are added. +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 3c946fc4..2725c62b 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