From 27803333e2f4d4d3f890c1ffd750923b5dc01bf5 Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Thu, 17 Sep 2026 12:41:51 +0200 Subject: [PATCH 1/3] Add extensible client and server transport APIs --- TRANSPORT_API_PLAN.md | 274 ++++++ docs/advanced/low-level-server.md | 6 + docs/client/transports.md | 36 + docs/handlers/context.md | 1 + docs/run/authorization.md | 17 +- docs/run/index.md | 25 + docs_src/authorization/tutorial003.py | 27 + docs_src/client_transports/tutorial005.py | 57 ++ docs_src/client_transports/tutorial006.py | 54 ++ examples/transports/README.md | 120 +++ examples/transports/brokers/mosquitto.acl | 11 + examples/transports/brokers/mosquitto.conf | 10 + .../brokers/rabbitmq-definitions.json | 13 + examples/transports/brokers/rabbitmq.conf | 2 + examples/transports/compose.yaml | 49 + examples/transports/demo_amqp.py | 38 + examples/transports/demo_common.py | 93 ++ examples/transports/demo_grpc.py | 71 ++ examples/transports/demo_grpc_features.py | 91 ++ examples/transports/demo_mqtt.py | 39 + .../mcp_transport_examples/__init__.py | 1 + .../mcp_transport_examples/_grpc_codec.py | 35 + .../transports/mcp_transport_examples/amqp.py | 138 +++ .../transports/mcp_transport_examples/grpc.py | 38 + .../mcp_transport_examples/grpc_client.py | 139 +++ .../mcp_transport_examples/grpc_context.py | 64 ++ .../mcp_transport_examples/grpc_response.py | 58 ++ .../mcp_transport_examples/grpc_server.py | 158 ++++ .../transports/mcp_transport_examples/mqtt.py | 125 +++ .../mcp_transport_examples/py.typed | 0 .../mcp_transport_examples/rpc.proto | 27 + .../mcp_transport_examples/rpc_pb2.py | 42 + .../mcp_transport_examples/rpc_pb2.pyi | 36 + examples/transports/pyproject.toml | 68 ++ .../reproduce_grpc_loop_shutdown.py | 37 + examples/transports/tests/__init__.py | 1 + ...ive_error_keeps_code_message_and_data.yaml | 19 + ...s_large_integers_and_extension_fields.yaml | 19 + ..._reaches_the_client_before_the_result.yaml | 19 + examples/transports/tests/conftest.py | 14 + examples/transports/tests/test_amqp.py | 30 + examples/transports/tests/test_grpc.py | 185 ++++ .../tests/test_grpc_cancel_signal.py | 72 ++ examples/transports/tests/test_grpc_client.py | 106 +++ .../tests/test_grpc_client_shutdown.py | 94 ++ .../transports/tests/test_grpc_context.py | 51 ++ .../transports/tests/test_grpc_lifecycle.py | 95 ++ .../transports/tests/test_grpc_response.py | 84 ++ examples/transports/tests/test_grpc_server.py | 220 +++++ .../tests/test_grpc_shutdown_order.py | 93 ++ examples/transports/tests/test_grpc_tls.py | 177 ++++ examples/transports/tests/test_mqtt.py | 28 + pyproject.toml | 4 +- src/mcp/client/_transport.py | 18 +- src/mcp/client/client.py | 10 +- src/mcp/server/context.py | 2 + src/mcp/server/lowlevel/server.py | 18 +- src/mcp/server/mcpserver/context.py | 6 + src/mcp/server/mcpserver/server.py | 10 + src/mcp/server/runner.py | 39 +- src/mcp/server/runtime.py | 158 ++++ src/mcp/shared/direct_dispatcher.py | 58 +- src/mcp/shared/dispatcher.py | 7 +- src/mcp/shared/transport.py | 67 ++ tests/client/test_client.py | 203 ++++- tests/docs_src/test_client_transports.py | 12 +- tests/server/test_runner.py | 60 +- tests/server/test_runtime.py | 634 +++++++++++++ tests/shared/test_dispatcher.py | 141 +++ uv.lock | 847 +++++++++++++++++- 70 files changed, 5556 insertions(+), 45 deletions(-) create mode 100644 TRANSPORT_API_PLAN.md create mode 100644 docs_src/authorization/tutorial003.py create mode 100644 docs_src/client_transports/tutorial005.py create mode 100644 docs_src/client_transports/tutorial006.py create mode 100644 examples/transports/README.md create mode 100644 examples/transports/brokers/mosquitto.acl create mode 100644 examples/transports/brokers/mosquitto.conf create mode 100644 examples/transports/brokers/rabbitmq-definitions.json create mode 100644 examples/transports/brokers/rabbitmq.conf create mode 100644 examples/transports/compose.yaml create mode 100644 examples/transports/demo_amqp.py create mode 100644 examples/transports/demo_common.py create mode 100644 examples/transports/demo_grpc.py create mode 100644 examples/transports/demo_grpc_features.py create mode 100644 examples/transports/demo_mqtt.py create mode 100644 examples/transports/mcp_transport_examples/__init__.py create mode 100644 examples/transports/mcp_transport_examples/_grpc_codec.py create mode 100644 examples/transports/mcp_transport_examples/amqp.py create mode 100644 examples/transports/mcp_transport_examples/grpc.py create mode 100644 examples/transports/mcp_transport_examples/grpc_client.py create mode 100644 examples/transports/mcp_transport_examples/grpc_context.py create mode 100644 examples/transports/mcp_transport_examples/grpc_response.py create mode 100644 examples/transports/mcp_transport_examples/grpc_server.py create mode 100644 examples/transports/mcp_transport_examples/mqtt.py create mode 100644 examples/transports/mcp_transport_examples/py.typed create mode 100644 examples/transports/mcp_transport_examples/rpc.proto create mode 100644 examples/transports/mcp_transport_examples/rpc_pb2.py create mode 100644 examples/transports/mcp_transport_examples/rpc_pb2.pyi create mode 100644 examples/transports/pyproject.toml create mode 100644 examples/transports/reproduce_grpc_loop_shutdown.py create mode 100644 examples/transports/tests/__init__.py create mode 100644 examples/transports/tests/cassettes/test_grpc/test_native_error_keeps_code_message_and_data.yaml create mode 100644 examples/transports/tests/cassettes/test_grpc/test_native_payload_keeps_large_integers_and_extension_fields.yaml create mode 100644 examples/transports/tests/cassettes/test_grpc/test_native_progress_reaches_the_client_before_the_result.yaml create mode 100644 examples/transports/tests/conftest.py create mode 100644 examples/transports/tests/test_amqp.py create mode 100644 examples/transports/tests/test_grpc.py create mode 100644 examples/transports/tests/test_grpc_cancel_signal.py create mode 100644 examples/transports/tests/test_grpc_client.py create mode 100644 examples/transports/tests/test_grpc_client_shutdown.py create mode 100644 examples/transports/tests/test_grpc_context.py create mode 100644 examples/transports/tests/test_grpc_lifecycle.py create mode 100644 examples/transports/tests/test_grpc_response.py create mode 100644 examples/transports/tests/test_grpc_server.py create mode 100644 examples/transports/tests/test_grpc_shutdown_order.py create mode 100644 examples/transports/tests/test_grpc_tls.py create mode 100644 examples/transports/tests/test_mqtt.py create mode 100644 src/mcp/server/runtime.py create mode 100644 src/mcp/shared/transport.py create mode 100644 tests/server/test_runtime.py diff --git a/TRANSPORT_API_PLAN.md b/TRANSPORT_API_PLAN.md new file mode 100644 index 0000000000..e462bd6d92 --- /dev/null +++ b/TRANSPORT_API_PLAN.md @@ -0,0 +1,274 @@ +# Extensible transport API plan + +Status: implementation in progress. New APIs still need final compatibility and native-binding review before release. + +## Progress + +| Work | Current evidence | Remaining | +| --- | --- | --- | +| Shared message contract | `mcp.shared.transport` exports stream, message, metadata, and transport types; existing client imports remain available | External adapter validation | +| Handler metadata | `TransportContext` reaches both handler APIs; concurrent principal-bound state, forged `_meta`, cross-peer replay, and missing-identity rejection are tested through the existing policy hook | Broker-specific identity-denial and cancellation scenarios | +| Multi-client lifecycle | Core tests cover shared lifespan, peer isolation, native readiness, and preservation of listener/startup failures across cleanup timeouts. `DirectDispatcher` now tracks operations on both peers and joins nested calls and handler cleanup; independent static review found no actionable issues | Final combined API and lifecycle review | +| Native dispatcher entry | Real gRPC binding uses protobuf envelopes with JSON payloads and no JSON-RPC frames. Maintained tests cover malformed traffic, capacity, cancellation, shutdown, error fidelity, subscriptions, and TLS. Both server APIs and multi-round trips also pass live examples on Python 3.10 and 3.14 | Final contract review, cross-platform validation, and the documented single-event-loop restriction | +| MQTT and AMQP adapters | Separate `examples/transports` package; both demos pass real two-peer calls for both server APIs and three client modes on Python 3.10 and 3.14; RabbitMQ cross-peer consumption is denied and borrowed channels survive cancellation | Full delivery/failure checks, MQTT provider limitations, record/replay, and adapter coverage | +| Release gates | Core: 6,027 tests pass with 100% branch coverage and `strict-no-cover`. Adapter package: 60 tests pass on Python 3.10 and 3.14. Every gRPC implementation and test file has 100% branch coverage on both interpreters. Ruff, both Pyright configurations, English docs, and all six HTTP conformance baseline legs pass | Whole adapter coverage remains 90%: AMQP is 31% and MQTT is 37%. Broker record/replay, remaining CI entries, and final API review remain | + +`ServerRuntime` replaces the unshipped `ServerHost` name following naming review. It is not an MCP host or network listener. `DispatcherTransport` replaces the planned client factory: the explicit wrapper avoids duplicating the `Client` constructor's options or guessing what an arbitrary context manager yields. Existing `Transport` objects still yield stream pairs. + +## Objective + +You can implement MQTT, AMQP, or gRPC adapters outside the SDK and use them with `Client`, low-level `Server`, and `MCPServer`. Adapters use only supported public APIs. They reuse MCP negotiation, validation, middleware, callbacks, and result handling instead of implementing those features again. + +There are two integration levels: + +- **Message transport:** carries MCP JSON-RPC messages over another communication channel. MQTT, AMQP, and a gRPC bidirectional stream can use this level. +- **Native binding:** maps MCP operations to another RPC system, such as gRPC methods and protobuf messages. It uses the dispatcher boundary rather than pretending to be a JSON-RPC stream. + +Supporting a native binding in the SDK does not make that binding an official MCP transport. Its wire format and interoperability claims need a separate specification review. + +## Scope and constraints + +- Preserve the existing `Transport` context manager and its stream-pair return value. +- Preserve `Client(...)`, `ClientSession(...)`, `Server.run(...)`, and built-in `MCPServer.run(...)` behavior. +- Add public entry points rather than removing or deprecating existing ones. +- Keep MQTT, AMQP, gRPC, and protobuf dependencies in adapter packages. +- Reuse `Dispatcher`, `JSONRPCDispatcher`, and the server runner functions. +- Do not introduce an interface with one method per MCP operation. +- Do not add a plugin registry, URL-scheme discovery, a universal broker configuration, or automatic request replay. +- Update relevant documentation with each public change. Do not add entries to the closed v1-to-v2 migration guide. + +## Existing foundations + +| Component | Existing extension point | Work needed | +| --- | --- | --- | +| `src/mcp/client/_transport.py` | Async context manager yielding `SessionMessage` streams | Document the full contract and expose supporting types through supported public imports | +| `src/mcp/client/client.py` | Accepts stream transports | Add an explicit lifecycle-managed dispatcher integration | +| `src/mcp/client/session.py` | Accepts `dispatcher=` | Preserve this entry point and make custom implementations supportable | +| `src/mcp/shared/dispatcher.py` | Request/notification boundary independent of wire encoding | Stabilize lifecycle, failures, ordering, and cancellation requirements | +| `src/mcp/server/runner.py` | Connection, stream, and single-request drivers | Expose hosting without requiring adapters to reconstruct the protocol pipeline | +| `src/mcp/server/lowlevel/server.py` | Runs one stream connection with its own lifespan | Support a host owning one lifespan across multiple peers | +| `src/mcp/server/mcpserver/server.py` | Built-in transport hosting | Make the same custom hosting surface available without private access | +| `src/mcp/shared/transport_context.py` | Transport-specific metadata | Connect it to actual user handler contexts | + +## Recommended ownership model + +| Layer | Owns | +| --- | --- | +| Adapter | Wire framing, physical connections, broker subscriptions, delivery settlement, routing, authenticated transport identity | +| Dispatcher | Request correlation, inbound scheduling, response delivery, notification ordering, progress and cancellation translation | +| MCP layer | Version negotiation, capability rules, typed validation, middleware, callbacks, result shaping | +| Server host | Application lifespan and supervision of active connections and requests | +| Application | Adapter configuration, authorization policy, and any operation-specific idempotency guarantees | + +A broker connection is not an MCP client connection. Each logical peer needs isolated request correlation and, for handshake-era protocols, isolated negotiated state. + +## Phase 1: Approve and pin the contracts + +### Deliverables + +- [ ] Inventory public imports, constructor forms, stream ownership, and observable failure behavior. Identify gaps in existing tests before adding more tests. +- [ ] Review real adapter call sites. Start with the [Google gRPC Python adapter](https://github.com/GoogleCloudPlatform/mcp-grpc-transport-py) and [Amazon MQ AMQP adapter](https://github.com/amazon-mq/mcp-amqp-transport). The latter is TypeScript and informs routing requirements, not Python API compatibility. +- [ ] Approve exact names and types for a shared transport import surface, an explicit client dispatcher factory, and a server hosting context. Names remain open until this review. +- [ ] Write complete client and server usage examples for both integration levels as design artifacts. Mark proposed calls as proposed until implemented. +- [ ] Define supported protocol versions and required features for each reference adapter. Start AMQP validation with AMQP 0.9.1 and MQTT validation with MQTT 5; do not imply support for other versions without testing them. +- [ ] Inspect the pinned conformance suite and map relevant existing scenarios to the work. SDK extension-point tests are separate from wire conformance tests. + +### Conformance evidence + +The pinned `@modelcontextprotocol/conformance@0.2.0-alpha.11` lists 69 frozen scenarios for 2026-07-28. Relevant shared-pipeline checks include `tools-call-with-progress`, `caching`, `request-metadata`, and the `input-required-result-*` scenarios. These existing features are reused, not reimplemented by the new extension points. + +Fresh local baseline runs pass for all six legs. Server 2026-07-28 has 151 passing checks and server 2025-11-25 has 84. Client 2026-07-28 has 387 and client 2025-11-25 has 224. The default server leg has 204 passing checks and 25 expected failures; the default client leg has 464 passing checks and nine expected failures. The existing baselines were not changed, and no solo retry was needed. These runs use the HTTP harness and do not certify custom broker or protobuf wire bindings. + +### Contract decisions + +Specify readiness, single-entry/re-entry rules, borrowed versus owned resources, EOF, cancellation, shutdown order, and failure propagation. Preserve current behavior on existing entry points. + +Distinguish malformed message observations, peer MCP errors, request timeouts, and terminal transport failures. A fatal receive or send failure must settle pending calls; yielding an exception item must not be mistaken for closing the channel. + +Define which notification ordering the dispatcher guarantees, including the existing receive-order intercept used by subscriptions. Do not require globally ordered request completion. + +Define how unsupported back-channels are reported. Transport capability never overrides a protocol-version prohibition. + +For any newly implemented 2026-07-28 feature, require a matching conformance-suite test. If none exists, stop that feature and report the missing test so an issue can be raised upstream. Do not silently substitute a local test for the repository's conformance requirement. + +### Exit condition + +A maintainer approves the contract and compatibility matrix before implementation. No proposed native wire binding is presented as standardized without evidence. + +## Phase 2: Expose shared types and transport metadata + +Depends on phase 1. + +### Shared transport deliverables + +- [x] Make `Transport`, stream types, message types, and required adapter metadata available through documented public imports. Keep existing imports working. +- [x] Carry `TransportContext` from adapters through dispatch to the actual `ServerRequestContext` and high-level `MCPServer` context. Add fields or properties without replacing handler argument types. +- [x] Expose the transport context builder on supported stream-hosting paths instead of requiring adapters to construct the internal dispatcher recipe. +- [ ] Preserve existing HTTP request access, headers, SSE callbacks, and unanswered-request settlement behavior. +- [x] Define a supported way to bind verified transport identity to a request. Audit request-state principal binding and context propagation so custom authentication does not accidentally become anonymous. + +### Shared transport acceptance checks + +- A handler can observe typed adapter metadata without a fake Starlette request or a private import. +- Concurrent requests from different principals retain the correct identity and metadata, including during cancellation. +- Broker-supplied reply destinations are authorized against the caller rather than trusted as arbitrary routing instructions. +- Existing HTTP, stdio, request-state, and handler-context tests remain unchanged and pass. + +## Phase 3: Add public multi-client server hosting + +Depends on phase 2. + +### Server runtime deliverables + +- [x] Add a hosting context available from both `Server` and `MCPServer`. It owns one application lifespan and exposes serving operations bound to that lifespan state. +- [x] Support one logical stream connection through the existing dual-era runner. Each connection retains its own protocol and correlation state. +- [ ] Define supervision: one peer disconnecting or sending malformed traffic does not cancel unrelated peers; a fatal listener failure is reported to the host owner. +- [x] Stop admission before shutdown, cancel and join active work, close connection resources, and finally exit application lifespan. Document resource-cleanup deadlines separately from cooperative handler joins. +- [ ] Keep connection/session admission limits and queue bounds configurable at the layer that owns them. Do not create an unbounded task per broker message. +- [ ] Preserve the existing single-connection `Server.run()` behavior. Avoid migrating all built-in hosting paths in the same change. + +### Server runtime acceptance checks + +- Two clients can both issue request ID `1` without cross-delivery or shared negotiation state. +- Application lifespan enters once and exits once while multiple connections come and go. +- One client can disconnect while another completes a call. +- Startup failure, idle connections, in-flight cancellation, and shutdown release resources without hanging. +- A custom adapter can host an `MCPServer` without accessing `_lowlevel_server`. + +## Phase 4: Validate message transports outside the SDK + +Depends on phase 3. Develop MQTT and AMQP adapters independently once the shared contract is settled. + +### Message transport deliverables + +- [x] Build or adapt an external-package-shaped MQTT 5 client and server adapter using only public SDK imports. +- [x] Build or adapt an AMQP 0.9.1 client and server adapter using only public SDK imports. +- [ ] Run the same client/server behavior checks through each adapter. Neither adapter may bypass the runner by calling tool methods directly. +- [ ] Document the wire binding, configuration, supported features, limits, failure behavior, and backend requirements for each adapter. + +### MQTT binding decisions + +Specify request and reply topics, subscription readiness, peer/session identity, and reply-topic authorization. Define QoS and duplicate handling. Do not retain command messages; define rejection of unexpected retained deliveries. Specify message expiry, disconnect detection, and reconnect behavior. + +### AMQP binding decisions + +Specify exchanges, queues, reply addresses, consumer prefetch, and publisher confirmation behavior. Define acknowledgment timing relative to request execution and response publication. Specify redelivery, poison-message handling, and expiry. Keep handshake-era traffic on the appropriate logical peer/worker; do not load-balance it blindly across independent sessions. + +### Delivery guarantees + +Broker delivery guarantees are not exactly-once tool execution. Document the crash window between a side effect, response publication, and message settlement. Do not retry arbitrary operations automatically. If deduplication is offered, define identity scope, retention, and behavior after process restart. + +Cancellation must reach the worker running the request. Reconnect must either restore explicitly supported state or fail the old calls and create a fresh logical connection. It must not silently replay calls. + +### Message transport acceptance checks + +Test a real broker for both adapters: multiple clients, out-of-order responses, duplicate delivery, disconnect/reconnect, backpressure, stale deliveries, and authenticated routing. Record supported external interactions and review recordings for secrets. Do not claim real broker behavior from handwritten mocks. + +Passing this phase establishes the JSON-RPC transport milestone. It does not complete native gRPC support. + +## Phase 5: Support native dispatcher integrations + +Depends on phases 1-3. This work can proceed alongside phase 4, but the final contract must incorporate findings from both paths. + +### Native dispatcher deliverables + +- [ ] Stabilize the custom `Dispatcher` lifecycle after reviewing the existing JSON-RPC and direct implementations and a native gRPC prototype. +- [x] Add an explicit `DispatcherTransport` wrapper accepting an async context manager yielding a dispatcher. Reuse existing client negotiation, caching, extensions, callback, and cleanup paths. Do not infer the integration type from an ambiguous context-manager return value. +- [x] Let the server runtime serve a dispatcher through the existing runner pipeline. Native requests retain inbound envelope/version validation at the untrusted entry boundary. +- [ ] Define native mappings for deadlines, transport cancellation, MCP errors, progress, notifications, request IDs, and subscriptions. Reuse existing call options; do not make HTTP-only options mandatory for native implementations. +- [x] Support required notification-intercept behavior and explicit request IDs used by subscriptions. Live regression tests cover acknowledgment/event routing, collisions, minted IDs, and ID reuse. +- [x] Verify arbitrary MCP method names and extension payloads survive the boundary. The gRPC binding carries arbitrary method names and preserves JSON integer precision; recorded calls also cover error codes outside int32. +- [ ] Preserve stream exception observations on the existing client paths. Define equivalent diagnostics for native transports without requiring `isinstance(JSONRPCDispatcher)` in third-party code. + +### Native dispatcher acceptance checks + +- The same high-level `Client` operations work through stream and native dispatcher integrations. +- Negotiation, multi-round-trip results, middleware, and result validation run through shared MCP code. +- Native cancellation and disconnects settle calls without requiring a fabricated JSON-RPC connection. +- MCP errors retain code, message, and data according to the approved mapping; gRPC status failures remain distinguishable where needed. +- The adapter has no duplicate client-session API and does not subclass `ClientSession` to reimplement every MCP method. + +## Phase 6: Validate native gRPC and publish the contract + +Depends on phases 4 and 5. + +### Native binding validation deliverables + +- [x] Implement a native gRPC client and server reference adapter with a documented protobuf-envelope binding. This is a reference binding, not interoperability with another project's schema. +- [x] Test notification/progress delivery, deadlines, cancellation, metadata, extension payloads, and error fidelity over a real gRPC connection. Replay checks are supplemented by tests executing the current server. +- [x] Document asyncio-only requirements for `grpc.aio`, including the single-loop restriction. Core extension points remain AnyIO-compatible; native adapters do not claim Trio support. +- [x] Validate both low-level `Server` and high-level `MCPServer` hosting, including concurrent peers and independent request delivery. +- [ ] Document stable import paths and complete runnable examples in the relevant existing pages: `docs/client/transports.md`, `docs/advanced/low-level-server.md`, `docs/run/index.md`, `docs/run/asgi.md`, and `docs/handlers/context.md`. Update lifespan, authorization, and client caching pages where their contracts are affected. +- [ ] Obtain fresh API-compatibility and adversarial lifecycle/security reviews. Reviewers should specifically challenge identity isolation, replay, callback deadlocks, and incomplete cleanup. + +### Native binding validation exit condition + +MQTT, AMQP, and native gRPC adapters work on both sides without private imports, duplicated MCP semantics, or new runtime dependencies in the core SDK. Compatibility and validation gates below pass. + +## Validation gates for every implementation slice + +Prefer existing public-API tests and add only missing behavior checks. Core lifecycle tests use in-memory execution, events, and bounded waits. Real transport semantics use real services. Keep test files aligned with the source tree and follow `.claude/skills/test-quality/SKILL.md`. + +Cover the following combinations where applicable: + +| Dimension | Cases | +| --- | --- | +| Server API | `Server`, `MCPServer` | +| Client integration | Existing stream transport, new dispatcher factory | +| Protocol | Legacy handshake, automatic discovery, pinned modern version | +| Messaging | Concurrent requests, peer errors, notifications, progress, subscriptions, extension methods | +| Lifecycle | Startup failure, timeout, peer cancellation, EOF, send failure, shutdown | +| Isolation | Repeated request IDs across peers, independent negotiated state, distinct identities | +| Delivery | Backpressure, duplicate and late messages, worker failure, reconnect | + +Transport bindings must document unsupported combinations rather than silently pass partial behavior as full support. + +Run repository checks with the frozen lockfile: + +```bash +uv run --frozen ruff check . +uv run --frozen ruff format --check . +uv run --frozen pyright +./scripts/test +``` + +Require 100% branch coverage and `strict-no-cover` for SDK changes. Run the existing client/server conformance jobs for both protocol eras and the default suite. These protect current wire behavior; they do not by themselves certify an MQTT, AMQP, or native protobuf binding. Validate cross-version and platform behavior in the repository CI matrix. + +## Review findings addressed + +A focused independent reviewer found two native lifecycle defects: cancellation was signalled only after handler cleanup, and a five-second join could abandon handlers before application lifespan closed. The server now signals cancellation before unwinding a handler task group. Both client and server join active work without abandoning it at a deadline. Live checks hold shielded cleanup beyond five seconds and prove that resources remain alive until it finishes. Shutdown can wait indefinitely for code that ignores cancellation; a process supervisor owns any hard termination deadline. + +Follow-up review confirmed those corrections and found a borrowed-channel closure race. It was reproduced through `Client.list_tools()` immediately after `channel.close()`, then corrected by consulting native channel state before constructing an RPC. The regression passes without making a network request. + +The `reviewer` Agent Hub profile is available. Earlier attempts used nonexistent profile names; subsequent focused reviews completed. Final review of the complete API and adapter work is still required. + +## Latest validation slice + +The four standalone gRPC shutdown programs are maintained AnyIO regressions under `examples/transports/tests/`. The tests execute real loopback servers and hold shielded cleanup beyond the former five-second join deadline. Additional cases exercise malformed frames, saturation, application and validation errors, callback isolation, request-ID collisions, and subscription routing. Cassette tests compare their recorded native results against the current in-process MCP handler, so replay does not leave their server handlers untested. + +`DirectDispatcher` previously returned from `run()` while a caller task was still unwinding a request or notification handler. Operations now register a cancellation scope and completion event on both peers. Closing either peer cancels them; `run()` joins completion before returning. Tests cover both closing peers, requests and notifications, nested back-channel calls, ordinary handler errors, and in-process client lifespan ordering. The dispatcher/client subset also passes all 108 tests on Python 3.10. + +The native adapter now exposes gRPC's verified `peer_identity_key` and immutable `peer_identities`. A fresh adversarial review found no direct spoofing path but requested stronger boundary assertions and authority documentation. Five live cases now cover plaintext, server-only TLS, mutual TLS, absent client certificates, and certificates signed by an untrusted key with the same issuer name. Middleware proves rejected peers never reach MCP dispatch. Accepted requests prove neither MCP client-info claims nor forged invocation metadata can supply or replace certificate identity. Empty identity does not prove plaintext, and names are not globally unique across independent trusted authorities. A fresh read-only closeout review found no remaining correctness, security, or test gaps in this TLS slice; it did not approve the combined API. + +TLS validation exposed a gRPC completion-queue limitation, reproduced without MCP in `examples/transports/reproduce_grpc_loop_shutdown.py`. With `grpcio==1.84.0` on macOS/Python 3.14.6, cancelled connectivity watches can complete after `channel.close()` and target a previously closed event loop. The adapter suite keeps one AnyIO runner for its session, while still closing per-test resources. The README records this support restriction, not an upstream fix; repeated loop lifetimes and final native-queue drainage are not certified. + +Current evidence is in `/tmp/mcp-core-final.log`, `/tmp/mcp-adapter-final310.log`, `/tmp/mcp-adapter-final314.log`, `/tmp/mcp-docs-final.log`, and `/tmp/mcp-conformance-final-{client,server}-*.log`. Coverage data uses `/tmp/mcp-adapter-final310` and `/tmp/mcp-adapter-final314`. Core coverage is 100%; total adapter coverage is 90%, with only MQTT/AMQP implementation gaps remaining. Generated protobuf implementation is excluded as compiler output, not handwritten adapter code. + +## Next implementation work + +1. Settle broker record/replay. `cassetter` has no MQTT/AMQP interceptor. Its gRPC wrapper also omits parts of streaming cancellation and matches only RPC methods; current tests supplement matching with serialized-request assertions and keep each cassette to one RPC. Do not substitute handwritten broker mocks to clear coverage. +2. Exercise broker redelivery, expiry, connection loss, malformed frames, cancellation, saturation, and TLS identity denial. Resolve aiomqtt's queue-overflow drops and discarded negative publish reason codes, or choose a provider with the required failure signals. Bound executing broker work, not only queued messages and connections. +3. Complete remaining CI matrix coverage and external adapter compatibility checks. Existing HTTP conformance baselines and local native coverage do not certify broker delivery semantics or another protobuf binding. +4. Obtain final independent compatibility and lifecycle/security reviews of the combined API and adapters. The scoped DirectDispatcher and TLS reviews are not approval of the entire change. Keep the contract provisional until those gates and maintainer approval are complete. + +The local Mosquitto and RabbitMQ fixtures are pinned by image digest. The temporary `mcp-sdk-transport-check` containers and network were removed after verification. Their public test credentials and ACLs are in `examples/transports/brokers/`; they are not production configuration. + +## Delivery order + +1. Approve the contract, compatibility matrix, and usage examples. +2. Expose shared types and context propagation in small reviewable changes. +3. Add server hosting with lifecycle and isolation tests. +4. Validate MQTT and AMQP adapters while implementing dispatcher integration. +5. Complete native gRPC validation and review the combined public contract. + +Each implementation change includes its tests and affected documentation. Do not defer coverage or lifecycle verification to the last phase. Do not publish the contract as stable until both message-based and native integrations have exercised it. diff --git a/docs/advanced/low-level-server.md b/docs/advanced/low-level-server.md index 5d49846b5f..01da00e3ed 100644 --- a/docs/advanced/low-level-server.md +++ b/docs/advanced/low-level-server.md @@ -65,6 +65,12 @@ The same text the `@mcp.tool()` version produced. Two honest differences: In a test you skip uvicorn and the port: `Client(server)` takes a low-level `Server` in-process exactly like it takes an `MCPServer`, and **[Testing](../get-started/testing.md)** is that pattern. +## Custom transports + +`Server.serve()` shares one application lifespan across multiple custom transport connections, just like `MCPServer.serve()`. Use the complete adapter example under [Running your server](../run/index.md#custom-transports). + +For a single connection, `Server.run(read_stream, write_stream, initialization_options, *, transport_builder=...)` remains available. The optional builder converts inbound message metadata into the `TransportContext` exposed as `ctx.transport`. Without it, stream dispatch supplies generic JSON-RPC metadata. Both paths retain the existing protocol-version handling; custom transport capabilities cannot enable features that the negotiated version forbids. + ## Nothing is checked for you `MCPServer` rejects a bad argument before your function ever runs, validating the call against the schema it generated (**[Tools](../servers/tools.md)**). diff --git a/docs/client/transports.md b/docs/client/transports.md index 6d9d30f90d..010c964a96 100644 --- a/docs/client/transports.md +++ b/docs/client/transports.md @@ -118,6 +118,8 @@ No subprocess, no port, no bytes on a wire. The client and the server are two ob The same form doubles as an embedding API: an application that constructs the server itself can call its tools without a network hop. +Closing the client cancels active in-process requests and waits for their handler cleanup before leaving application lifespan. A caller interrupted by connection closure receives `MCPError` with code `CONNECTION_CLOSED`. Handlers and callbacks must cooperate with cancellation; shielded cleanup keeps the application's resources alive until it finishes. + ## SSE `sse_client(url)`, from `mcp.client.sse`, is the HTTP transport that Streamable HTTP superseded. Wrap it the same way, `Client(sse_client("http://localhost:8000/sse"))`, to talk to a server that still speaks it, and don't build anything new on it. @@ -128,6 +130,40 @@ To `Client`, all of the above are the same thing. A **transport** is any async context manager that yields a `(read, write)` pair of message streams: formally, the `Transport` protocol in `mcp.client`. `Client` resolves its argument by type: a `str` becomes `streamable_http_client(url)`, a `StdioServerParameters` becomes `stdio_client(params)`, a server object connects in-process, and anything else is entered as a transport directly. That last rule is why `stdio_client(...)`, `streamable_http_client(...)` and `sse_client(...)` all drop into the same slot, and why you can write your own. +### Implement a message transport + +```python title="custom_transport.py" +--8<-- "docs_src/client_transports/tutorial005.py" +``` + +This example implements an in-memory adapter with two independent clients. A network adapter uses the same `TransportStreams` contract and replaces the memory channels with message readers and writers. You import the contract and its supporting types from `mcp.shared.transport`; the existing `mcp.client.Transport` import still works. + +Each stream pair represents **one logical peer**, not an entire broker. The adapter owns framing, routing, and its network resources. The SDK owns negotiation, request correlation, and MCP validation. + +Entering a transport opens its channel. Exiting stops its background tasks and closes resources it owns. The SDK also closes streams during connection shutdown, so their `aclose()` methods must be safe to call more than once. A network client supplied by the application remains owned by the application. + +An inbound item is a decoded `SessionMessage` or an exception describing a recoverable message error. An exception item alone does not disconnect the peer. End the read stream on connection loss so pending calls fail instead of waiting indefinitely. Make writes cancellable and apply backpressure rather than buffering without a bound. + +!!! warning "Delivery is not execution" + MQTT or AMQP delivery guarantees do not make a tool execute exactly once. A redelivered request can repeat a side effect. Define expiry, duplicate handling, and reconnect behavior in the adapter; do not silently replay unfinished calls. + +The server side of this example uses `server.serve()`. Its lifecycle and connection limits are covered under [Custom transports](../run/index.md#custom-transports). The repository's `examples/transports/README.md` contains live MQTT 5 and AMQP 0.9.1 examples, their binding rules, and the validation still needed before production use. + +### Integrate a native dispatcher + +```python title="dispatcher_transport.py" +--8<-- "docs_src/client_transports/tutorial006.py" +``` + +`DispatcherTransport` explicitly wraps an async context manager yielding a `Dispatcher`. `Client` enters that context, starts the dispatcher, and uses its ordinary MCP negotiation, callbacks, caching, and validation. It stops the dispatcher before exiting the connection context. You configure the client through the same constructor; there is no separate native client-session API. + +The example uses the SDK's `DirectDispatcher`. The repository's `examples/transports/README.md` also contains a real gRPC implementation with protobuf envelopes and JSON payloads. Native network bindings implement this dispatcher boundary instead of creating `SessionMessage` streams. The connection context acquires the transport resources; it must yield an unstarted dispatcher because the SDK owns `run()`. + +On the server, `runtime.connect(DispatcherTransport(...))` serves the modern per-request-envelope protocol. It rejects the legacy initialize handshake. Use `mode="auto"` or a supported modern version on the client. Message transports still support both eras. Native dispatchers supply their own contexts, so this server path rejects `session_id=` and `transport_builder=`. + +!!! warning "Native bindings remain experimental" + The custom `Dispatcher` lifecycle is still provisional pending validation against native network adapters. This wrapper is not an official gRPC wire binding. Define and test framing, cancellation, error mapping, notifications, and extension payloads in your adapter before claiming interoperability. + ## Recap * `Client("http://.../mcp")` (a URL) connects over Streamable HTTP, the production transport. diff --git a/docs/handlers/context.md b/docs/handlers/context.md index f43521aa05..a09b78f94a 100644 --- a/docs/handlers/context.md +++ b/docs/handlers/context.md @@ -63,6 +63,7 @@ The injected object is small. Besides `request_id`: * `await ctx.report_progress(progress, total, message)`: stream progress back to the caller during a long call. The whole story is in **[Progress](progress.md)**. * `await ctx.elicit(message, schema)` and `await ctx.elicit_url(...)`: pause the tool and ask the user a question. That's **[Elicitation](elicitation.md)**. * `ctx.session`: the server's side of the conversation with this client. Notifications you send to the client live here; the last section uses it. +* `ctx.transport`: transport metadata supplied by the dispatcher. Custom adapters can attach a `TransportContext` subclass; see [Custom transports](../run/index.md#custom-transports). The SDK populates it for dispatched requests; manually constructed request contexts may leave it `None`. It does not change the existing `ctx.headers` behavior. * `ctx.headers`: the request headers the transport carried, or `None` on stdio. Read a custom header with `(ctx.headers or {}).get("x-...")`. Headers are client-supplied input - fine for a locale or a feature flag, never an identity. * `ctx.request_context`: the raw per-request record. The field you'll reach for is `lifespan_context`, the object your startup code yielded (see **[Lifespan](lifespan.md)**). diff --git a/docs/run/authorization.md b/docs/run/authorization.md index fefd0ed34a..59299825c8 100644 --- a/docs/run/authorization.md +++ b/docs/run/authorization.md @@ -42,7 +42,7 @@ The SDK has no opinion about what a valid token looks like. You tell it, by impl ## What you get over HTTP -Authorization lives in HTTP headers, so it exists only on the HTTP transports. Run it on the one you deploy: `mcp.run(transport="streamable-http")` puts it on `http://127.0.0.1:8000/mcp`, and **[Running your server](index.md)** has the rest. The app now has two routes: +The SDK's built-in OAuth integration uses HTTP headers, so it applies only to HTTP transports. Run it on the one you deploy: `mcp.run(transport="streamable-http")` puts it on `http://127.0.0.1:8000/mcp`, and **[Running your server](index.md)** has the rest. The app now has two routes: ```text /mcp @@ -104,6 +104,21 @@ Call `whoami` with `Authorization: Bearer alice-token` and the model reads: alice (scopes: notes:read) ``` +## Custom transport identities + +```python title="server.py" +--8<-- "docs_src/authorization/tutorial003.py" +``` + +Have your adapter attach `VerifiedPeer` only after authenticating the caller. `runtime.connect(transport_builder=...)` passes that metadata to handlers as `ctx.transport`. Use a stable, namespaced principal that distinguishes the issuing authority and user, not a display name or a client-supplied `_meta` field. + +The existing `RequestStateSecurity.bind_principal` hook binds sealed request state to this identity. Another principal cannot replay it. Raising when verified metadata is absent prevents state from silently becoming anonymous. This hook protects multi-round-trip state; it does not authenticate connections or authorize ordinary tool calls. Those checks still belong at the adapter boundary and in your application policy. + +The generated key suits a single process. Share keys across workers when retries can reach another instance, as described in [Protecting request state](../handlers/multi-round-trip.md#protecting-requeststate). + +!!! warning "Broker credentials are not publisher identity" + A service's broker credentials authenticate the service, not every publisher. Bind peers through broker-enforced topic or queue permissions, or verify an end-user credential yourself. Validate reply destinations before sending data. The SDK's `get_access_token()` remains an HTTP OAuth helper; custom transport metadata does not populate it automatically. + ## The half the SDK doesn't do The SDK gives you the resource-server half: verify, advertise, refuse. It does not give you a login page, a consent screen, or a token. diff --git a/docs/run/index.md b/docs/run/index.md index fb23b4bb54..e162ff4d01 100644 --- a/docs/run/index.md +++ b/docs/run/index.md @@ -88,6 +88,31 @@ Each transport has its own keyword arguments, all on `run()`: `run()` is the short road. The moment you need more (your server mounted inside an existing app, two servers in one process, CORS for browser clients), you build the ASGI app yourself and hand it to any ASGI host. That is **[Add to an existing app](asgi.md)**. +## Custom transports + +```python title="custom_transport.py" +--8<-- "docs_src/client_transports/tutorial005.py" +``` + +`server.serve()` returns a context manager yielding a `ServerRuntime`. It starts application lifespan once and shares that state across the connections you supply. Both `MCPServer` and the low-level `Server` expose this API. It does not open a network listener or connect to a broker. + +Call `await runtime.connect(transport)` for each logical peer. The runtime opens the transport and serves it in the background. For message streams, the call returns when the transport is open, before MCP negotiation. For a dispatcher transport, it also waits for the dispatcher to signal readiness. Each peer has its own request-ID state; message streams negotiate their protocol era independently. + +| Option | Behavior | +| --- | --- | +| `server.serve(max_connections=100)` | Limits active connections. `connect()` waits for capacity before opening another transport. | +| `runtime.connect(..., transport_builder=...)` | Builds each inbound message's `TransportContext`, available as `ctx.transport` in handlers. | +| `runtime.connect(..., session_id=...)` | Supplies an optional identifier for a handshake-era connection. It is not authentication. | + +The default connection limit prevents an adapter from opening unlimited peers. Await admission in your listener instead of spawning unbounded tasks that wait for a slot. Message-size limits, broker queue limits, and per-peer request limits remain the adapter's responsibility. + +An error opening a transport reaches the caller of `connect()`. A later connection failure is logged and closes that peer without cancelling other peers. Exiting `server.serve()` stops admission, cancels active work, closes transports, and then exits application lifespan. Transport cleanup and lifespan cleanup each have a five-second cancellation deadline; cleanup code must cooperate with cancellation. Cleanup timeouts do not suppress an earlier listener or dispatcher startup failure. Dispatchers must join their handlers before returning; these deadlines do not permit closing application resources while a handler still uses them. Code that ignores cancellation can delay that join, so enforce hard process deadlines outside the SDK. Do not retain a runtime after its context exits. + +!!! warning "A peer label is not an identity" + The example attaches a label for demonstration. A real adapter must authenticate and authorize callers before binding identity to a request. Authenticating your server's broker connection does not authenticate every publisher. Validate reply destinations instead of forwarding messages to arbitrary client-supplied topics or queues. + +The [client transport contract](../client/transports.md#implement-a-message-transport) describes message types, resource ownership, and connection loss. For a native RPC binding, `runtime.connect()` also accepts an explicit [dispatcher transport](../client/transports.md#integrate-a-native-dispatcher). That entry serves modern per-request envelopes, not legacy handshakes. The built-in `run()` forms remain unchanged. + ## Server settings A couple of things about running are not about the transport. They are constructor arguments: diff --git a/docs_src/authorization/tutorial003.py b/docs_src/authorization/tutorial003.py new file mode 100644 index 0000000000..72867892d7 --- /dev/null +++ b/docs_src/authorization/tutorial003.py @@ -0,0 +1,27 @@ +import secrets +from dataclasses import dataclass + +from mcp.server import ServerRequestContext +from mcp.server.mcpserver import MCPServer +from mcp.server.request_state import RequestStateSecurity +from mcp.shared.transport import TransportContext + + +@dataclass(kw_only=True, frozen=True) +class VerifiedPeer(TransportContext): + principal: str + + +def principal(ctx: ServerRequestContext) -> str: + if not isinstance(ctx.transport, VerifiedPeer): + raise ValueError("Verified transport identity is required") + return ctx.transport.principal + + +mcp = MCPServer( + "broker-service", + request_state_security=RequestStateSecurity( + keys=[secrets.token_bytes(32)], + bind_principal=principal, + ), +) diff --git a/docs_src/client_transports/tutorial005.py b/docs_src/client_transports/tutorial005.py new file mode 100644 index 0000000000..796744ba4d --- /dev/null +++ b/docs_src/client_transports/tutorial005.py @@ -0,0 +1,57 @@ +from collections.abc import AsyncIterator +from contextlib import asynccontextmanager +from dataclasses import dataclass +from typing import Any + +import anyio + +from mcp import Client +from mcp.server.mcpserver import Context, MCPServer +from mcp.server.runtime import ServerRuntime +from mcp.shared.memory import create_client_server_memory_streams +from mcp.shared.transport import MessageMetadata, TransportContext, TransportStreams + + +@dataclass(kw_only=True, frozen=True) +class PeerContext(TransportContext): + peer: str + + +server = MCPServer("Custom transport") + + +@server.tool() +async def identify(ctx: Context) -> str: + transport = ctx.transport + assert isinstance(transport, PeerContext) + return transport.peer + + +@asynccontextmanager +async def memory_client(runtime: ServerRuntime[Any], peer: str) -> AsyncIterator[TransportStreams]: + async with create_client_server_memory_streams() as (client_streams, server_streams): + + @asynccontextmanager + async def server_transport() -> AsyncIterator[TransportStreams]: + async with server_streams[0], server_streams[1]: + yield server_streams + + def build_context(metadata: MessageMetadata) -> PeerContext: + return PeerContext(kind="memory", can_send_request=True, peer=peer) + + await runtime.connect(server_transport(), transport_builder=build_context) + yield client_streams + + +async def main() -> None: + async with server.serve(max_connections=10) as runtime: + async with Client(memory_client(runtime, "alice")) as alice: + async with Client(memory_client(runtime, "bob")) as bob: + alice_result = await alice.call_tool("identify") + bob_result = await bob.call_tool("identify") + assert alice_result.structured_content == {"result": "alice"} + assert bob_result.structured_content == {"result": "bob"} + + +if __name__ == "__main__": + anyio.run(main) diff --git a/docs_src/client_transports/tutorial006.py b/docs_src/client_transports/tutorial006.py new file mode 100644 index 0000000000..f87cae91a0 --- /dev/null +++ b/docs_src/client_transports/tutorial006.py @@ -0,0 +1,54 @@ +from collections.abc import AsyncIterator +from contextlib import asynccontextmanager +from typing import Any + +import anyio + +from mcp import Client +from mcp.server.mcpserver import Context, MCPServer +from mcp.server.runtime import ServerRuntime +from mcp.shared.direct_dispatcher import create_direct_dispatcher_pair +from mcp.shared.dispatcher import Dispatcher +from mcp.shared.transport import DispatcherTransport, TransportContext + +server = MCPServer("Dispatcher transport") + + +@server.tool() +async def greet(name: str, ctx: Context) -> str: + assert ctx.transport is not None + assert not ctx.transport.can_send_request + return f"Hello, {name}!" + + +def direct_client(runtime: ServerRuntime[Any]) -> DispatcherTransport: + @asynccontextmanager + async def connection() -> AsyncIterator[Dispatcher[TransportContext]]: + client_dispatcher, server_dispatcher = create_direct_dispatcher_pair() + + @asynccontextmanager + async def server_connection() -> AsyncIterator[Dispatcher[TransportContext]]: + try: + yield server_dispatcher + finally: + server_dispatcher.close() + + try: + await runtime.connect(DispatcherTransport(server_connection())) + yield client_dispatcher + finally: + client_dispatcher.close() + server_dispatcher.close() + + return DispatcherTransport(connection()) + + +async def main() -> None: + async with server.serve() as runtime: + async with Client(direct_client(runtime)) as client: + result = await client.call_tool("greet", {"name": "Alice"}) + assert result.structured_content == {"result": "Hello, Alice!"} + + +if __name__ == "__main__": + anyio.run(main) diff --git a/examples/transports/README.md b/examples/transports/README.md new file mode 100644 index 0000000000..d8e06446bf --- /dev/null +++ b/examples/transports/README.md @@ -0,0 +1,120 @@ +# Reference custom transports + +```bash +docker compose -p mcp-sdk-transport-check -f examples/transports/compose.yaml up --wait +UV_PROJECT_ENVIRONMENT=examples/transports/.venv uv sync --frozen --package mcp-transport-examples --group dev +UV_PROJECT_ENVIRONMENT=examples/transports/.venv uv run --frozen --package mcp-transport-examples python examples/transports/demo_mqtt.py +UV_PROJECT_ENVIRONMENT=examples/transports/.venv uv run --frozen --package mcp-transport-examples python examples/transports/demo_amqp.py +docker compose -p mcp-sdk-transport-check -f examples/transports/compose.yaml down --volumes +``` + +Run these commands from the repository root. Both programs exit successfully only after checking concurrent calls for two peers, both `Server` and `MCPServer`, and `legacy`, `auto`, and pinned `2026-07-28` clients. The server lifespan starts once per run, not once per peer. + +The adapters import only public SDK APIs. They live in a separate workspace package so installing `mcp` does not install MQTT or AMQP dependencies. They are reference implementations under development, not production-ready transports or official MCP wire bindings. The native gRPC binding is described below. + +## Native gRPC + +```bash +UV_PROJECT_ENVIRONMENT=examples/transports/.venv uv run --frozen --package mcp-transport-examples python examples/transports/demo_grpc.py +UV_PROJECT_ENVIRONMENT=examples/transports/.venv uv run --frozen --package mcp-transport-examples python examples/transports/demo_grpc_features.py +UV_PROJECT_ENVIRONMENT=examples/transports/.venv uv run --frozen --package mcp-transport-examples --group dev pytest -c examples/transports/pyproject.toml examples/transports/tests/test_grpc*.py --record-mode=none +``` + +These programs start their own loopback gRPC listener. No broker is needed. `grpc_client(channel)` and `grpc_server(listener)` return `DispatcherTransport` objects for the existing client and server runtime APIs. You own the channel and listener; runtime shutdown stops MCP handlers without taking ownership of other gRPC services on the listener. + +Register the binding through `runtime.connect(grpc_server(listener))` before starting the listener. Use one MCP binding per gRPC server. The server adapter rejects excess work at `max_requests`, which defaults to 64, rather than queuing unlimited waiting handlers. + +The binding serves modern per-request MCP envelopes. It has no legacy initialize handshake. Each MCP request is a native server-streaming RPC on `/mcp.transport.example.MCP/Call`; gRPC correlates calls and provides deadlines and cancellation. The auxiliary request ID supports MCP subscription correlation and stays local to each client's calls. + +`mcp_transport_examples/rpc.proto` defines protobuf envelopes with JSON-encoded parameters, results, and error data. There is no JSON-RPC envelope. JSON payloads preserve arbitrary extension fields and integer precision, which protobuf `Struct` would otherwise lose through its floating-point number representation. This is an example binding, not compatibility with another project's gRPC schema. + +Notifications precede one terminal result or error, followed by end-of-stream. Progress, subscription acknowledgments, and change events use the originating RPC's response stream. Unsolicited notifications without a request channel are unsupported. Ordinary MCP errors preserve their code, message, and data. Native deadline failures become `REQUEST_TIMEOUT`; other gRPC failures become `CONNECTION_CLOSED` with the original status exception as their cause. + +The examples and regression tests check both server APIs, progress, subscriptions, multi-round-trip results, concurrent clients with colliding request IDs, caller cancellation, deadlines, runtime shutdown, borrowed-channel closure, and client shutdown during a blocked callback. Cancellation is signalled before handler cleanup begins. Active handlers and callbacks are joined before their owning resources close, including shielded cleanup that takes longer than five seconds. Code that ignores cancellation indefinitely can therefore hold shutdown indefinitely; enforce a hard process deadline in your supervisor rather than closing resources under running code. The SDK's five-second transport and application cleanup deadlines do not replace this join. Invocation metadata and socket peer addresses are not authenticated principals. + +Regenerate the protobuf bindings with the pinned compiler: + +```bash +UV_PROJECT_ENVIRONMENT=examples/transports/.venv uv run --frozen --package mcp-transport-examples --group dev python -m grpc_tools.protoc --proto_path=examples/transports --python_out=examples/transports --pyi_out=examples/transports examples/transports/mcp_transport_examples/rpc.proto +``` + +### TLS and peer identity + +```bash +UV_PROJECT_ENVIRONMENT=examples/transports/.venv uv run --frozen --package mcp-transport-examples --group dev pytest -c examples/transports/pyproject.toml examples/transports/tests/test_grpc_tls.py --record-mode=none +``` + +These tests create temporary certificate authorities and real local TLS endpoints. They check mutual TLS, server-only TLS, and plaintext connections. Missing or untrusted client certificates cannot reach MCP middleware when the listener requires client authentication. Forged MCP client information and gRPC invocation metadata do not change the verified identity. + +Configure TLS through your gRPC channel and listener credentials. Set `require_client_auth=True` on `grpc.ssl_server_credentials()` when clients must present a certificate. Handlers receive `GRPCContext.peer_identity_key` and `peer_identities` from gRPC's native authentication context. Without client authentication, these are `None` and an empty tuple, including on encrypted server-only TLS connections. + +Certificate validation is not application authorization. The identity values do not identify their issuing authority. If you trust independent authorities that can issue the same common name or subject alternative name, do not treat that name as a globally unique principal. Choose a trust-domain namespace and certificate-issuance policy before using these values with `RequestStateSecurity.bind_principal`. An issuer-name string or untrusted invocation metadata cannot supply that trust boundary. + +### Event-loop lifetime + +```bash +UV_PROJECT_ENVIRONMENT=examples/transports/.venv uv run --frozen --package mcp-transport-examples python examples/transports/reproduce_grpc_loop_shutdown.py +``` + +This diagnostic intentionally fails when a native completion targets a closed loop. It reproduces the limitation without importing MCP. The failure was observed with `grpcio==1.84.0` on macOS and Python 3.14.6; the assertion includes the runtime versions. + +Keep one long-lived asyncio event loop per process or test worker. Create, use, and close all gRPC resources on that loop. Repeated `anyio.run()` or `asyncio.run()` lifetimes are outside this adapter's current support. gRPC's process-wide completion queue can deliver cancelled connectivity-watch callbacks after `channel.close()` returns. Joining MCP handlers does not drain those native callbacks. The adapter tests keep one AnyIO runner alive for the session; they do not suppress loop errors or claim an upstream correction. + +## Connection ownership + +`demo_mqtt.py` and `demo_amqp.py` contain complete adapter setup. `demo_common.py` runs the same MCP application through either transport. + +Each side owns and enters its network client or AMQP channel before entering the transport. Keep the server's network resources alive until `server.serve()` exits. The transport unsubscribes or cancels its consumer, but does not close a borrowed client or channel. + +The examples provision a dedicated network connection per logical peer. Both sides agree on a fresh session identifier out of band. They do not implement service discovery, dynamic peer acceptance, or multiplexing many peers through one MQTT messages iterator. Those choices belong in an adapter, not in the SDK dispatcher. + +## Wire bindings + +| Property | MQTT 5 | AMQP 0.9.1 | +| --- | --- | --- | +| Requests | `mcp///requests` | `mcp...requests` | +| Replies | `mcp///responses` | `mcp...responses` | +| Framing | One JSON-RPC message per publish | One JSON-RPC message per delivery; `application/json` content type | +| Delivery | QoS 2 only | Publisher confirmations; acknowledge before SDK handoff | +| Close | Empty payload | Empty JSON-typed message body | +| Retention | Never retain; do not receive stored retained messages | Nondurable, auto-delete queues | +| Expiry | MQTT message expiry, default 60 seconds | Message TTL and unused queue expiry, default 60 seconds | +| Duplicate handling | MQTT QoS 2 handles protocol retransmissions within its session | Reject redeliveries without requeue | +| Reconnect | Fail pending work; establish a fresh logical connection | Fail pending work; establish a fresh logical connection | + +Always use fresh topics or queue names after reconnecting. Do not reuse JSON-RPC request IDs across multiple peers in one SDK stream pair. Server-initiated messages, progress, and cancellation use the same pair of directions; the SDK applies protocol-version restrictions. + +Both adapters reject messages larger than `max_message_size`, which defaults to 4 MiB. Malformed messages become recoverable stream exceptions. Connection loss closes the receive stream so the SDK can fail pending calls. + +## Delivery limits + +QoS 2 and publisher confirmations describe broker delivery, not exactly-once tool execution. Republishing a JSON-RPC request is a new delivery and can repeat a side effect. Neither adapter retries calls automatically. + +The AMQP adapter deliberately acknowledges before handing a message to the SDK. A process failure in that window can lose work. Rejecting broker redeliveries avoids automatically rerunning uncertain work, but is not a replacement for application idempotency. + +RabbitMQ queues hold at most 256 ready messages and reject publication on overflow. Consumer prefetch bounds unacknowledged deliveries. The MQTT example bounds aiomqtt's incoming queue at 256 messages; aiomqtt can drop messages when that queue fills, so configure client request timeouts and monitor its overflow warnings. This remains a limitation to resolve before claiming reliable saturation behavior. Its publish callback also discards MQTT negative reason codes, so broker rejection may surface only as an MCP request timeout. Neither setting bounds the number of concurrently executing tool handlers. + +## Authentication + +The local brokers are configured with per-user topic or queue permissions. RabbitMQ cross-peer response consumption was also checked live and rejected. Mosquitto can acknowledge a subscription even when its ACL prevents delivery, so a successful SUBACK is not proof of permission. MQTT authorization-denial checks remain to be automated. The server attaches the principal associated with its configured route; it does not accept an arbitrary reply destination from the message payload. + +For production, use TLS and your broker's credential and authorization policy. Include the issuing authority and user in a stable principal identifier. Use `RequestStateSecurity.bind_principal` to bind multi-round-trip state to verified metadata; the SDK does not automatically convert broker identity into an HTTP OAuth token. + +The Compose fixtures use public test credentials, listen only on localhost, and disable durable storage. Do not deploy these broker configurations. You can change the local ports with `MQTT_TEST_PORT` and `AMQP_TEST_PORT`; the defaults are 13883 and 15672 respectively. + +Both libraries use asyncio. The MQTT provider requires a selector event loop on Windows. Trio and Windows adapter validation have not been completed. + +## Validation status + +```bash +UV_PROJECT_ENVIRONMENT=examples/transports/.venv uv run --frozen --package mcp-transport-examples --group dev pytest -c examples/transports/pyproject.toml examples/transports/tests --record-mode=none +uv run --frozen pyright --project examples/transports +``` + +The broker unit tests check configuration failures without opening network connections. The broker programs verify real traffic against Mosquitto and RabbitMQ; they are not cassette-backed CI coverage. + +The gRPC tests record real calls with `cassetter` and replay with `--record-mode=none`. They check payload fidelity, progress, and application errors. The tests also compare the serialized requests with the recording: the current gRPC matcher matches only the RPC method, which is insufficient for a generic MCP binding. Each cassette contains one RPC to avoid replaying a newly recorded call as the response to a different request during recording. + +The lifecycle, capacity, and malformed-frame regression tests own a gRPC server inside the test process. That server is the software under test, not an external service; replaying its outputs would bypass the behavior being checked. The cassette tests separately compare recorded native results with the current in-process MCP handler and verify request-body fidelity. `cassetter` also lacks parts of the streaming-call cancellation interface, so it is not used to stand in for these live server tests. Binary protobuf payloads are not pattern-scrubbed; inspect new cassettes before committing them. The checked-in recordings contain only public test data. + +`cassetter` has no MQTT or AMQP interceptor. Broker record/replay, complete broker-adapter coverage, broker TLS, and cross-platform checks remain open gates. The gRPC implementation and its regression tests have full line and branch coverage on the locally checked interpreters; that does not establish interoperability with another binding or support for repeated event-loop lifetimes. Do not treat a successful live program or cassette replay as evidence for untested server behavior. diff --git a/examples/transports/brokers/mosquitto.acl b/examples/transports/brokers/mosquitto.acl new file mode 100644 index 0000000000..2872feb502 --- /dev/null +++ b/examples/transports/brokers/mosquitto.acl @@ -0,0 +1,11 @@ +user server +topic read mcp/+/+/requests +topic write mcp/+/+/responses + +user alice +topic write mcp/alice/+/requests +topic read mcp/alice/+/responses + +user bob +topic write mcp/bob/+/requests +topic read mcp/bob/+/responses diff --git a/examples/transports/brokers/mosquitto.conf b/examples/transports/brokers/mosquitto.conf new file mode 100644 index 0000000000..751d1a2784 --- /dev/null +++ b/examples/transports/brokers/mosquitto.conf @@ -0,0 +1,10 @@ +listener 1883 +allow_anonymous false +password_file /tmp/passwords +acl_file /mosquitto/config/mosquitto.acl +persistence false +log_dest stdout +log_type all +max_packet_size 4195328 +max_inflight_messages 32 +max_queued_messages 256 diff --git a/examples/transports/brokers/rabbitmq-definitions.json b/examples/transports/brokers/rabbitmq-definitions.json new file mode 100644 index 0000000000..03811d0fb8 --- /dev/null +++ b/examples/transports/brokers/rabbitmq-definitions.json @@ -0,0 +1,13 @@ +{ + "users": [ + {"name": "server", "password_hash": "dGVzdISQMYLFRDkVmIyH/2iPR3elfgPHcZO7uFXGqTE0UeU9", "hashing_algorithm": "rabbit_password_hashing_sha256", "tags": []}, + {"name": "alice", "password_hash": "dGVzdMhyAjoCEMSETc4cmiL+/OInoknje5+9BVEZ8SaVlj+Z", "hashing_algorithm": "rabbit_password_hashing_sha256", "tags": []}, + {"name": "bob", "password_hash": "dGVzdFu3/YOufljrGKLUVvAqIh2R9WeQ87Ql039pWvRL9HZO", "hashing_algorithm": "rabbit_password_hashing_sha256", "tags": []} + ], + "vhosts": [{"name": "/"}], + "permissions": [ + {"user": "server", "vhost": "/", "configure": ".*", "write": ".*", "read": ".*"}, + {"user": "alice", "vhost": "/", "configure": "^mcp\\.alice\\..*", "write": "^(amq\\.default|mcp\\.alice\\.[^.]+\\.requests)$", "read": "^mcp\\.alice\\.[^.]+\\.responses$"}, + {"user": "bob", "vhost": "/", "configure": "^mcp\\.bob\\..*", "write": "^(amq\\.default|mcp\\.bob\\.[^.]+\\.requests)$", "read": "^mcp\\.bob\\.[^.]+\\.responses$"} + ] +} diff --git a/examples/transports/brokers/rabbitmq.conf b/examples/transports/brokers/rabbitmq.conf new file mode 100644 index 0000000000..57c11bfd94 --- /dev/null +++ b/examples/transports/brokers/rabbitmq.conf @@ -0,0 +1,2 @@ +definitions.import_backend = local_filesystem +definitions.local.path = /etc/rabbitmq/definitions.json diff --git a/examples/transports/compose.yaml b/examples/transports/compose.yaml new file mode 100644 index 0000000000..26c1fa1b32 --- /dev/null +++ b/examples/transports/compose.yaml @@ -0,0 +1,49 @@ +services: + amqp: + image: rabbitmq:4.1.8-alpine@sha256:1a087dd3a29b91448407409df70f4f6cb213ac0c269a62861bfe2a665f4ced03 + ports: + - "127.0.0.1:${AMQP_TEST_PORT:-15672}:5672" + volumes: + - ./brokers/rabbitmq.conf:/etc/rabbitmq/rabbitmq.conf:ro + - ./brokers/rabbitmq-definitions.json:/etc/rabbitmq/definitions.json:ro + healthcheck: + test: ["CMD", "rabbitmq-diagnostics", "-q", "check_port_connectivity"] + interval: 2s + timeout: 5s + retries: 30 + mqtt: + image: eclipse-mosquitto:2.0.22@sha256:212f89e1eaeb2c322d6441b64396e3346026674db8fa9c27beac293405c32b3c + ports: + - "127.0.0.1:${MQTT_TEST_PORT:-13883}:1883" + volumes: + - ./brokers/mosquitto.conf:/mosquitto/config/mosquitto.conf:ro + - ./brokers/mosquitto.acl:/mosquitto/config/mosquitto.acl:ro + entrypoint: ["/bin/sh", "-ec"] + command: + - | + mosquitto_passwd -b -c /tmp/passwords server test-server-password + mosquitto_passwd -b /tmp/passwords alice test-alice-password + mosquitto_passwd -b /tmp/passwords bob test-bob-password + chmod 644 /tmp/passwords + exec mosquitto -c /mosquitto/config/mosquitto.conf + healthcheck: + test: + [ + "CMD", + "mosquitto_pub", + "-h", + "127.0.0.1", + "-u", + "server", + "-P", + "test-server-password", + "-t", + "mcp/alice/health/responses", + "-m", + "", + "-q", + "2", + ] + interval: 1s + timeout: 3s + retries: 20 diff --git a/examples/transports/demo_amqp.py b/examples/transports/demo_amqp.py new file mode 100644 index 0000000000..828e3f33a9 --- /dev/null +++ b/examples/transports/demo_amqp.py @@ -0,0 +1,38 @@ +"""Exercise the AMQP 0.9.1 adapter against the local RabbitMQ broker.""" + +import os +from contextlib import AsyncExitStack + +import aio_pika +import anyio +from mcp.shared.transport import Transport + +from demo_common import verify +from mcp_transport_examples.amqp import amqp_transport + + +async def open_transport(stack: AsyncExitStack, principal: str, session: str, server_side: bool) -> Transport: + user = "server" if server_side else principal + connection = await aio_pika.connect( + host="127.0.0.1", + port=int(os.environ.get("AMQP_TEST_PORT", "15672")), + login=user, + password=f"test-{user}-password", + ) + await stack.enter_async_context(connection) + channel = await connection.channel(publisher_confirms=True) + stack.push_async_callback(channel.close) + queue = f"mcp.{principal}.{session}" + incoming, outgoing = ("requests", "responses") if server_side else ("responses", "requests") + return amqp_transport(channel, incoming_queue=f"{queue}.{incoming}", outgoing_queue=f"{queue}.{outgoing}") + + +async def main() -> None: + for highlevel in (False, True): + for mode in ("legacy", "auto", "2026-07-28"): + with anyio.fail_after(5): + await verify(open_transport, kind="amqp", highlevel=highlevel, mode=mode) + + +if __name__ == "__main__": + anyio.run(main) diff --git a/examples/transports/demo_common.py b/examples/transports/demo_common.py new file mode 100644 index 0000000000..58a2b4af64 --- /dev/null +++ b/examples/transports/demo_common.py @@ -0,0 +1,93 @@ +"""Shared live-broker checks for the reference adapters.""" + +from collections.abc import AsyncIterator, Awaitable, Callable +from contextlib import AsyncExitStack, asynccontextmanager +from dataclasses import dataclass +from functools import partial +from typing import Any, TypeAlias +from uuid import uuid4 + +import anyio +from mcp import Client +from mcp.server import Server, ServerRequestContext +from mcp.server.mcpserver import Context, MCPServer +from mcp.shared.transport import MessageMetadata, Transport, TransportContext +from mcp.types import CallToolRequestParams, CallToolResult, ListToolsResult, PaginatedRequestParams, TextContent, Tool + +TransportFactory: TypeAlias = Callable[[AsyncExitStack, str, str, bool], Awaitable[Transport]] + + +@dataclass(kw_only=True, frozen=True) +class BrokerContext(TransportContext): + principal: str + + +def peer_context(metadata: MessageMetadata, *, principal: str, kind: str) -> BrokerContext: + return BrokerContext(kind=kind, can_send_request=True, principal=principal) + + +async def verify(factory: TransportFactory, *, kind: str, highlevel: bool, mode: str) -> None: + """Check real concurrent calls, peer metadata, both server APIs, and shared lifespan.""" + entered = {"alice": anyio.Event(), "bob": anyio.Event()} + lifecycle: list[str] = [] + + async def identity(transport: TransportContext | None) -> str: + assert isinstance(transport, BrokerContext) + principal = transport.principal + entered[principal].set() + await entered["bob" if principal == "alice" else "alice"].wait() + return principal + + @asynccontextmanager + async def lifespan(server: Server[Any] | MCPServer[Any]) -> AsyncIterator[None]: + lifecycle.append("start") + try: + yield None + finally: + lifecycle.append("stop") + + if highlevel: + server = MCPServer("Broker", lifespan=lifespan) + + @server.tool() + async def identify(ctx: Context) -> str: + return await identity(ctx.transport) + + else: + + async def list_tools(ctx: ServerRequestContext, params: PaginatedRequestParams | None) -> ListToolsResult: + return ListToolsResult(tools=[Tool(name="identify", input_schema={"type": "object"})]) + + async def call_tool(ctx: ServerRequestContext, params: CallToolRequestParams) -> CallToolResult: + assert params.name == "identify" + return CallToolResult(content=[TextContent(text=await identity(ctx.transport))]) + + server = Server("Broker", lifespan=lifespan, on_list_tools=list_tools, on_call_tool=call_tool) + + results: dict[str, str] = {} + + async def call(client: Client, principal: str) -> None: + result = await client.call_tool("identify") + content = result.content[0] + assert isinstance(content, TextContent) + results[principal] = content.text + + async with AsyncExitStack() as stack: + sessions = {principal: uuid4().hex for principal in entered} + transports = { + principal: await factory(stack, principal, session, True) for principal, session in sessions.items() + } + runtime = await stack.enter_async_context(server.serve()) + clients: dict[str, Client] = {} + for principal, transport in transports.items(): + await runtime.connect(transport, transport_builder=partial(peer_context, principal=principal, kind=kind)) + client_transport = await factory(stack, principal, sessions[principal], False) + clients[principal] = await stack.enter_async_context( + Client(client_transport, mode=mode, read_timeout_seconds=5) + ) + async with anyio.create_task_group() as tg: + for principal, client in clients.items(): + tg.start_soon(call, client, principal) + assert results == {"alice": "alice", "bob": "bob"} + assert lifecycle == ["start"] + assert lifecycle == ["start", "stop"] diff --git a/examples/transports/demo_grpc.py b/examples/transports/demo_grpc.py new file mode 100644 index 0000000000..ad14db06de --- /dev/null +++ b/examples/transports/demo_grpc.py @@ -0,0 +1,71 @@ +"""Exercise the native gRPC binding over a real loopback connection.""" + +from contextlib import AsyncExitStack + +import anyio +import grpc.aio +from mcp import Client +from mcp.server import Server, ServerRequestContext +from mcp.server.mcpserver import Context, MCPServer +from mcp.types import CallToolRequestParams, CallToolResult, ListToolsResult, PaginatedRequestParams, TextContent, Tool + +from mcp_transport_examples.grpc import grpc_client, grpc_server +from mcp_transport_examples.grpc_context import GRPCContext + + +async def verify(highlevel: bool, mode: str) -> None: + if highlevel: + server = MCPServer("Native gRPC") + + @server.tool() + async def echo(value: str, ctx: Context) -> str: + assert isinstance(ctx.transport, GRPCContext) + assert not ctx.transport.can_send_request + await ctx.report_progress(1, 2, "halfway") + return value + + else: + + async def list_tools(ctx: ServerRequestContext, params: PaginatedRequestParams | None) -> ListToolsResult: + return ListToolsResult(tools=[Tool(name="echo", input_schema={"type": "object"})]) + + async def call_tool(ctx: ServerRequestContext, params: CallToolRequestParams) -> CallToolResult: + assert params.name == "echo" + assert isinstance(ctx.transport, GRPCContext) + assert params.arguments is not None + await ctx.session.report_progress(1, 2, "halfway") + return CallToolResult(content=[TextContent(text=str(params.arguments["value"]))]) + + server = Server("Native gRPC", on_list_tools=list_tools, on_call_tool=call_tool) + + updates: list[tuple[float, float | None, str | None]] = [] + + async def progress(progress: float, total: float | None, message: str | None) -> None: + updates.append((progress, total, message)) + + async with AsyncExitStack() as stack: + listener = grpc.aio.server() + port = listener.add_insecure_port("127.0.0.1:0") + stack.push_async_callback(listener.stop, 0) + runtime = await stack.enter_async_context(server.serve()) + await runtime.connect(grpc_server(listener)) + await listener.start() + channel = await stack.enter_async_context(grpc.aio.insecure_channel(f"127.0.0.1:{port}")) + client = await stack.enter_async_context(Client(grpc_client(channel), mode=mode)) + value = "MCP without a JSON-RPC envelope" + result = await client.call_tool("echo", {"value": value}, progress_callback=progress) + content = result.content[0] + assert isinstance(content, TextContent) + assert content.text == value + assert updates == [(1, 2, "halfway")] + + +async def main() -> None: + for highlevel in (False, True): + for mode in ("auto", "2026-07-28"): + with anyio.fail_after(5): + await verify(highlevel, mode) + + +if __name__ == "__main__": + anyio.run(main) diff --git a/examples/transports/demo_grpc_features.py b/examples/transports/demo_grpc_features.py new file mode 100644 index 0000000000..7418ecaaba --- /dev/null +++ b/examples/transports/demo_grpc_features.py @@ -0,0 +1,91 @@ +"""Live checks for subscriptions and multi-round-trip results over native gRPC.""" + +from contextlib import AsyncExitStack + +import anyio +import grpc.aio +from mcp import Client +from mcp.client import ClientRequestContext +from mcp.client.subscriptions import ToolsListChanged +from mcp.server.mcpserver import Context, MCPServer +from mcp.types import ElicitRequest, ElicitRequestFormParams, ElicitRequestParams, ElicitResult, InputRequiredResult + +from mcp_transport_examples.grpc import grpc_client, grpc_server + + +async def verify() -> None: + server = MCPServer("native features") + entered = {"alice": anyio.Event(), "bob": anyio.Event()} + + @server.tool() + async def overlap(label: str, ctx: Context) -> str: + assert ctx.request_context.request_id == 0 + entered[label].set() + await entered["bob" if label == "alice" else "alice"].wait() + return label + + @server.tool() + async def announce(ctx: Context) -> str: + await ctx.notify_tools_changed() + return "announced" + + @server.tool() + async def confirm(ctx: Context) -> str | InputRequiredResult: + if ctx.input_responses is not None: + assert ctx.request_state == "awaiting confirmation" + answer = ctx.input_responses["confirm"] + assert isinstance(answer, ElicitResult) + assert answer.action == "accept" + return "confirmed" + return InputRequiredResult( + input_requests={ + "confirm": ElicitRequest( + params=ElicitRequestFormParams( + message="Confirm?", requested_schema={"type": "object", "properties": {}} + ) + ) + }, + request_state="awaiting confirmation", + ) + + async def elicit(context: ClientRequestContext, params: ElicitRequestParams) -> ElicitResult: + return ElicitResult(action="accept", content={}) + + async with AsyncExitStack() as stack: + listener = grpc.aio.server() + port = listener.add_insecure_port("127.0.0.1:0") + stack.push_async_callback(listener.stop, 0) + runtime = await stack.enter_async_context(server.serve()) + await runtime.connect(grpc_server(listener)) + await listener.start() + channel = await stack.enter_async_context(grpc.aio.insecure_channel(f"127.0.0.1:{port}")) + client = await stack.enter_async_context(Client(grpc_client(channel), elicitation_callback=elicit)) + async with client.listen(tools_list_changed=True) as subscription: + result = await client.call_tool("announce") + assert result.structured_content == {"result": "announced"} + event = await anext(subscription) + assert isinstance(event, ToolsListChanged) + result = await client.call_tool("confirm") + assert result.structured_content == {"result": "confirmed"} + + clients: dict[str, Client] = {} + for label in entered: + peer_channel = await stack.enter_async_context(grpc.aio.insecure_channel(f"127.0.0.1:{port}")) + clients[label] = await stack.enter_async_context(Client(grpc_client(peer_channel), mode="2026-07-28")) + + async def call(label: str, client: Client) -> None: + result = await client.call_tool("overlap", {"label": label}) + assert result.structured_content == {"result": label} + + async with anyio.create_task_group() as tg: + for label, peer in clients.items(): + tg.start_soon(call, label, peer) + + +async def main() -> None: + with anyio.fail_after(5): + await verify() + + +if __name__ == "__main__": + anyio.run(main) diff --git a/examples/transports/demo_mqtt.py b/examples/transports/demo_mqtt.py new file mode 100644 index 0000000000..55b1a96cee --- /dev/null +++ b/examples/transports/demo_mqtt.py @@ -0,0 +1,39 @@ +"""Exercise the MQTT 5 adapter against the local Mosquitto broker.""" + +import os +from contextlib import AsyncExitStack + +import aiomqtt +import anyio +from mcp.shared.transport import Transport + +from demo_common import verify +from mcp_transport_examples.mqtt import mqtt_transport + + +async def open_transport(stack: AsyncExitStack, principal: str, session: str, server_side: bool) -> Transport: + user = "server" if server_side else principal + client = await stack.enter_async_context( + aiomqtt.Client( + "127.0.0.1", + int(os.environ.get("MQTT_TEST_PORT", "13883")), + username=user, + password=f"test-{user}-password", + protocol=aiomqtt.ProtocolVersion.V5, + max_queued_incoming_messages=256, + ) + ) + topic = f"mcp/{principal}/{session}" + incoming, outgoing = ("requests", "responses") if server_side else ("responses", "requests") + return mqtt_transport(client, incoming_topic=f"{topic}/{incoming}", outgoing_topic=f"{topic}/{outgoing}") + + +async def main() -> None: + for highlevel in (False, True): + for mode in ("legacy", "auto", "2026-07-28"): + with anyio.fail_after(5): + await verify(open_transport, kind="mqtt", highlevel=highlevel, mode=mode) + + +if __name__ == "__main__": + anyio.run(main) diff --git a/examples/transports/mcp_transport_examples/__init__.py b/examples/transports/mcp_transport_examples/__init__.py new file mode 100644 index 0000000000..a9a2c5b3bb --- /dev/null +++ b/examples/transports/mcp_transport_examples/__init__.py @@ -0,0 +1 @@ +__all__ = [] diff --git a/examples/transports/mcp_transport_examples/_grpc_codec.py b/examples/transports/mcp_transport_examples/_grpc_codec.py new file mode 100644 index 0000000000..63b8fa47ca --- /dev/null +++ b/examples/transports/mcp_transport_examples/_grpc_codec.py @@ -0,0 +1,35 @@ +"""JSON payloads inside the experimental protobuf binding.""" + +from __future__ import annotations + +import json +from typing import Any, NoReturn, cast + +MAX_PAYLOAD_SIZE = 4 * 1024 * 1024 +RPC_METHOD = "/mcp.transport.example.MCP/Call" + + +def encode_json(value: Any) -> bytes: + payload = json.dumps(value, allow_nan=False, ensure_ascii=True, separators=(",", ":")).encode("utf-8") + if len(payload) > MAX_PAYLOAD_SIZE: + raise ValueError("Payload exceeds the gRPC binding's size limit") + return payload + + +def decode_json(payload: bytes) -> Any: + if len(payload) > MAX_PAYLOAD_SIZE: + raise ValueError("Payload exceeds the gRPC binding's size limit") + return json.loads(payload, parse_constant=reject_constant) + + +def decode_object(payload: bytes, *, nullable: bool = False) -> dict[str, Any] | None: + value = decode_json(payload) + if isinstance(value, dict): + return cast("dict[str, Any]", value) + if nullable and value is None: + return None + raise ValueError("Expected a JSON object") + + +def reject_constant(value: str) -> NoReturn: + raise ValueError(f"Invalid JSON constant: {value}") diff --git a/examples/transports/mcp_transport_examples/amqp.py b/examples/transports/mcp_transport_examples/amqp.py new file mode 100644 index 0000000000..98573d8b01 --- /dev/null +++ b/examples/transports/mcp_transport_examples/amqp.py @@ -0,0 +1,138 @@ +"""A symmetric AMQP 0.9.1 transport for one logical MCP peer.""" + +from __future__ import annotations + +from collections.abc import AsyncIterator +from contextlib import asynccontextmanager, suppress +from dataclasses import dataclass +from types import TracebackType + +import anyio +from aio_pika import Message +from aio_pika.abc import AbstractChannel, AbstractIncomingMessage +from aio_pika.exceptions import AMQPError, ChannelInvalidStateError +from mcp.shared.transport import SessionMessage, TransportStreams +from mcp.types import jsonrpc_message_adapter +from pamqp.common import Arguments +from pydantic import ValidationError +from typing_extensions import Self + + +@asynccontextmanager +async def amqp_transport( + channel: AbstractChannel, + *, + incoming_queue: str, + outgoing_queue: str, + expiry: int = 60, + max_message_size: int = 4 * 1024 * 1024, +) -> AsyncIterator[TransportStreams]: + """Connect a peer over two dedicated queues without automatic replay. + + You own `channel` and its connection. Use a fresh queue pair for every + logical connection and restrict queue access with broker permissions. + Messages are acknowledged before SDK handoff; redeliveries are rejected. + This avoids automatically repeating side effects but can lose work after + acknowledgment. A publisher confirmation is not tool completion. + + Raises: + ValueError: If routing, limits, or publisher-confirm settings are invalid. + AMQPError: If queue setup or publication fails. + """ + if not incoming_queue or not outgoing_queue or incoming_queue == outgoing_queue: + raise ValueError("AMQP directions must use different nonempty queue names") + if expiry < 1 or max_message_size < 1 or not channel.publisher_confirms: + raise ValueError("Positive limits and publisher confirmations are required") + arguments: Arguments = {"x-expires": expiry * 1000, "x-max-length": 256, "x-overflow": "reject-publish"} + incoming = await channel.declare_queue(incoming_queue, auto_delete=True, arguments=arguments) + await channel.declare_queue(outgoing_queue, auto_delete=True, arguments=arguments) + await channel.set_qos(prefetch_count=16) + send, receive = anyio.create_memory_object_stream[SessionMessage | Exception](0) + writer = _AMQPWriter(channel, outgoing_queue, expiry, max_message_size) + + lock = anyio.Lock() + active: set[anyio.Event] = set() + + def channel_closed(sender: object, exc: BaseException | None) -> None: + send.close() + + async def deliver(message: AbstractIncomingMessage) -> None: + finished = anyio.Event() + active.add(finished) + try: + async with lock: + if message.redelivered: + await message.reject(requeue=False) + await send.send(ValueError("AMQP redelivery is not replayed")) + return + await message.ack() + if len(message.body) > max_message_size or message.content_type != "application/json": + await send.send(ValueError("Rejected oversized or non-JSON AMQP message")) + return + if not message.body: + send.close() + return + try: + decoded = jsonrpc_message_adapter.validate_json(message.body, by_name=False) + except ValidationError as exc: + await send.send(exc) + else: + await send.send(SessionMessage(decoded)) + except (AMQPError, ChannelInvalidStateError): + send.close() + except (anyio.BrokenResourceError, anyio.ClosedResourceError): + pass + finally: + active.remove(finished) + finished.set() + + async with send, receive: + channel.close_callbacks.add(channel_closed) + try: + tag = await incoming.consume(deliver, exclusive=True) + try: + async with writer: + yield receive, writer + finally: + send.close() + with anyio.move_on_after(1, shield=True), suppress(AMQPError, ChannelInvalidStateError): + await incoming.cancel(tag) + for finished in tuple(active): + await finished.wait() + finally: + channel.close_callbacks.discard(channel_closed) + + +@dataclass +class _AMQPWriter: + channel: AbstractChannel + queue: str + expiry: int + max_message_size: int + closed: bool = False + + async def send(self, item: SessionMessage, /) -> None: + if self.closed: + raise anyio.ClosedResourceError + payload = item.message.model_dump_json(by_alias=True, exclude_none=True).encode() + if len(payload) > self.max_message_size: + raise ValueError("Encoded MCP message exceeds max_message_size") + await self.channel.default_exchange.publish( + Message(payload, content_type="application/json", expiration=self.expiry), routing_key=self.queue + ) + + async def aclose(self) -> None: + if not self.closed: + self.closed = True + with anyio.move_on_after(1, shield=True), suppress(AMQPError, ChannelInvalidStateError): + await self.channel.default_exchange.publish( + Message(b"", content_type="application/json", expiration=self.expiry), routing_key=self.queue + ) + + async def __aenter__(self) -> Self: + return self + + async def __aexit__( + self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None + ) -> None: + await self.aclose() diff --git a/examples/transports/mcp_transport_examples/grpc.py b/examples/transports/mcp_transport_examples/grpc.py new file mode 100644 index 0000000000..4f353a1af2 --- /dev/null +++ b/examples/transports/mcp_transport_examples/grpc.py @@ -0,0 +1,38 @@ +"""Factories for the experimental native protobuf transport.""" + +from __future__ import annotations + +from collections.abc import AsyncIterator +from contextlib import asynccontextmanager + +import grpc.aio +from mcp.shared.dispatcher import Dispatcher +from mcp.shared.transport import DispatcherTransport, TransportContext + +from mcp_transport_examples.grpc_client import GRPCClientDispatcher +from mcp_transport_examples.grpc_server import GRPCServerDispatcher + + +def grpc_client(channel: grpc.aio.Channel) -> DispatcherTransport: + """Use a borrowed channel with `Client`; the caller owns TLS, credentials, and channel closure.""" + + @asynccontextmanager + async def connection() -> AsyncIterator[Dispatcher[TransportContext]]: + yield GRPCClientDispatcher(channel) + + return DispatcherTransport(connection()) + + +def grpc_server(server: grpc.aio.Server, *, max_requests: int = 64) -> DispatcherTransport: + """Attach one MCP binding to a borrowed server before starting its listener. + + Connect this transport to `ServerRuntime`, then start the gRPC server. + The caller owns the listener and other registered gRPC services. Runtime + shutdown cancels MCP handlers without stopping unrelated services. + """ + + @asynccontextmanager + async def connection() -> AsyncIterator[Dispatcher[TransportContext]]: + yield GRPCServerDispatcher(server, max_requests=max_requests) + + return DispatcherTransport(connection()) diff --git a/examples/transports/mcp_transport_examples/grpc_client.py b/examples/transports/mcp_transport_examples/grpc_client.py new file mode 100644 index 0000000000..9d804334a8 --- /dev/null +++ b/examples/transports/mcp_transport_examples/grpc_client.py @@ -0,0 +1,139 @@ +"""A native gRPC dispatcher that reuses the SDK's high-level client.""" + +from __future__ import annotations + +import asyncio +from collections.abc import Mapping +from typing import Any + +import anyio +import anyio.abc +import grpc +import grpc.aio +from mcp.shared.dispatcher import ( + CallOptions, + OnNotify, + OnNotifyIntercept, + OnRequest, + coerce_request_id, +) +from mcp.shared.exceptions import MCPError, NoBackChannelError +from mcp.types import CONNECTION_CLOSED, REQUEST_TIMEOUT, ErrorData, RequestId + +from mcp_transport_examples._grpc_codec import RPC_METHOD, decode_object, encode_json +from mcp_transport_examples.grpc_context import GRPCContext, GRPCDispatchContext +from mcp_transport_examples.grpc_response import PendingCall, receive_response +from mcp_transport_examples.rpc_pb2 import CallEvent, CallRequest + + +class GRPCClientDispatcher: + """Run MCP calls on a borrowed gRPC channel, with one response stream per request.""" + + def __init__(self, channel: grpc.aio.Channel) -> None: + self._channel = channel + self._rpc = channel.unary_stream( + RPC_METHOD, request_serializer=CallRequest.SerializeToString, response_deserializer=CallEvent.FromString + ) + self._on_notify: OnNotify | None = None + self._intercept: OnNotifyIntercept | None = None + self._calls: dict[RequestId, PendingCall] = {} + self._next_id = 0 + self._closed = False + + async def run( + self, + on_request: OnRequest, + on_notify: OnNotify, + on_notify_intercept: OnNotifyIntercept | None = None, + *, + task_status: anyio.abc.TaskStatus[None] = anyio.TASK_STATUS_IGNORED, + ) -> None: + """Enable requests and cancel active RPCs when the client session exits.""" + self._on_notify = on_notify + self._intercept = on_notify_intercept + task_status.started() + try: + state = self._channel.get_state() + while state != grpc.ChannelConnectivity.SHUTDOWN: + await self._channel.wait_for_state_change(state) + state = self._channel.get_state() + finally: + self._closed = True + self._on_notify = None + pending = tuple(self._calls.values()) + for request in pending: + request.scope.cancel() + request.call.cancel() + with anyio.CancelScope(shield=True): + for request in pending: + await request.done.wait() + + async def send_raw_request( + self, method: str, params: Mapping[str, Any] | None, opts: CallOptions | None = None + ) -> dict[str, Any]: + """Send a native RPC and route its notifications before returning the final result. + + Raises: + MCPError: A peer error, request timeout, or closed connection. + """ + if self._closed or self._channel.get_state() == grpc.ChannelConnectivity.SHUTDOWN: + raise MCPError(code=CONNECTION_CLOSED, message="Connection closed") + on_notify = self._on_notify + if on_notify is None: + raise RuntimeError("GRPCClientDispatcher.run() has not started") + opts = opts or {} + request_id = opts.get("request_id") + if request_id is None: + while self._next_id in self._calls: + self._next_id += 1 + request_id = self._next_id + self._next_id += 1 + key = coerce_request_id(request_id) + if key in self._calls: + raise ValueError(f"Request id {request_id!r} is already in flight") + request = CallRequest( + method=method, + params_json=encode_json(params), + request_id_json=encode_json(request_id), + report_progress="on_progress" in opts, + ) + call = self._rpc(request, timeout=opts.get("timeout")) + pending = PendingCall(call) + self._calls[key] = pending + complete = False + terminal: CallEvent | None = None + dctx = GRPCDispatchContext(GRPCContext(kind="grpc", can_send_request=False, peer="server"), None, self.notify) + try: + with pending.scope, anyio.fail_after(opts.get("timeout")): + terminal = await receive_response(call, dctx, opts, on_notify, self._intercept) + complete = True + if terminal is None: + raise MCPError(code=CONNECTION_CLOSED, message="Connection closed") + if terminal.WhichOneof("payload") == "error_json": + raise MCPError.from_error_data(ErrorData.model_validate(decode_object(terminal.error_json))) + result = decode_object(terminal.result_json) + assert result is not None + return result + except grpc.aio.AioRpcError as exc: + code = REQUEST_TIMEOUT if exc.code() == grpc.StatusCode.DEADLINE_EXCEEDED else CONNECTION_CLOSED + raise MCPError( + code=code, message="gRPC request timed out" if code == REQUEST_TIMEOUT else "gRPC connection failed" + ) from exc + except ValueError as exc: + raise MCPError(code=CONNECTION_CLOSED, message="Invalid gRPC response") from exc + except TimeoutError as exc: + raise MCPError(code=REQUEST_TIMEOUT, message="gRPC request timed out") from exc + except asyncio.CancelledError: + if self._closed or self._channel.get_state() == grpc.ChannelConnectivity.SHUTDOWN: + raise MCPError(code=CONNECTION_CLOSED, message="Connection closed") from None + raise + finally: + self._calls.pop(key) + if not complete: + call.cancel() + pending.done.set() + + async def notify(self, method: str, params: Mapping[str, Any] | None, opts: CallOptions | None = None) -> None: + """The modern native binding uses structural cancellation, not client notifications.""" + if not self._closed: + raise NoBackChannelError(method) diff --git a/examples/transports/mcp_transport_examples/grpc_context.py b/examples/transports/mcp_transport_examples/grpc_context.py new file mode 100644 index 0000000000..7f93be868a --- /dev/null +++ b/examples/transports/mcp_transport_examples/grpc_context.py @@ -0,0 +1,64 @@ +"""Request-scoped metadata and notifications for the native gRPC binding.""" + +from __future__ import annotations + +from collections.abc import Awaitable, Callable, Mapping +from dataclasses import dataclass, field +from typing import Any + +import anyio +from mcp.shared.dispatcher import CallOptions +from mcp.shared.exceptions import NoBackChannelError +from mcp.shared.transport import MessageMetadata, TransportContext +from mcp.types import RequestId + + +@dataclass(kw_only=True, frozen=True) +class GRPCContext(TransportContext): + """gRPC peer information, including identities verified by the configured transport. + + Invocation metadata is untrusted. Peer identities are empty on insecure + connections; the application decides which verified identities to authorize. + """ + + peer: str + metadata: tuple[tuple[str, str | bytes], ...] = () + peer_identity_key: str | None = None + peer_identities: tuple[bytes, ...] = () + + +@dataclass +class GRPCDispatchContext: + """Notifications are scoped to one RPC; server-initiated requests are unavailable.""" + + transport: GRPCContext + request_id: RequestId | None + send_notification: Callable[[str, Mapping[str, Any] | None], Awaitable[None]] + report_progress: bool = False + message_metadata: MessageMetadata = None + cancel_requested: anyio.Event = field(default_factory=anyio.Event) + + @property + def can_send_request(self) -> bool: + """The modern binding has no server-initiated request channel.""" + return False + + async def send_raw_request( + self, method: str, params: Mapping[str, Any] | None, opts: CallOptions | None = None + ) -> dict[str, Any]: + """Reject requests on this request-scoped channel.""" + raise NoBackChannelError(method) + + async def notify(self, method: str, params: Mapping[str, Any] | None, opts: CallOptions | None = None) -> None: + """Deliver a notification on the originating RPC.""" + await self.send_notification(method, params) + + async def progress(self, progress: float, total: float | None = None, message: str | None = None) -> None: + """Send progress only when the caller requested it.""" + if self.report_progress: + params: dict[str, Any] = {"progressToken": self.request_id, "progress": progress} + if total is not None: + params["total"] = total + if message is not None: + params["message"] = message + await self.notify("notifications/progress", params) diff --git a/examples/transports/mcp_transport_examples/grpc_response.py b/examples/transports/mcp_transport_examples/grpc_response.py new file mode 100644 index 0000000000..c72bbe1fd6 --- /dev/null +++ b/examples/transports/mcp_transport_examples/grpc_response.py @@ -0,0 +1,58 @@ +"""Consume native response streams and deliver request-scoped notifications.""" + +from __future__ import annotations + +import logging +from collections.abc import AsyncIterable +from dataclasses import dataclass, field + +import anyio +import grpc.aio +from mcp.shared.dispatcher import CallOptions, OnNotify, OnNotifyIntercept, run_notify_intercept +from mcp.types import ProgressNotificationParams + +from mcp_transport_examples._grpc_codec import decode_object +from mcp_transport_examples.grpc_context import GRPCDispatchContext +from mcp_transport_examples.rpc_pb2 import CallEvent, CallRequest + +logger = logging.getLogger(__name__) + + +@dataclass(frozen=True) +class PendingCall: + """Track both the native RPC and callbacks executing in its caller's task.""" + + call: grpc.aio.UnaryStreamCall[CallRequest, CallEvent] + scope: anyio.CancelScope = field(default_factory=anyio.CancelScope) + done: anyio.Event = field(default_factory=anyio.Event) + + +async def receive_response( + events: AsyncIterable[CallEvent], + context: GRPCDispatchContext, + opts: CallOptions, + on_notify: OnNotify, + intercept: OnNotifyIntercept | None, +) -> CallEvent: + """Deliver notifications in receive order, then require exactly one terminal event followed by EOF.""" + terminal: CallEvent | None = None + async for event in events: + kind = event.WhichOneof("payload") + if terminal is not None or kind is None: + raise ValueError("Invalid gRPC response sequence") + if kind != "notification": + terminal = event + continue + notification = event.notification + data = decode_object(notification.params_json, nullable=True) + if notification.method == "notifications/progress" and "on_progress" in opts: + progress = ProgressNotificationParams.model_validate(data, by_name=False) + try: + await opts["on_progress"](progress.progress, progress.total, progress.message) + except Exception: + logger.exception("Progress callback failed") + if not run_notify_intercept(intercept, notification.method, data): + await on_notify(context, notification.method, data) + if terminal is None: + raise ValueError("gRPC call ended without an MCP result") + return terminal diff --git a/examples/transports/mcp_transport_examples/grpc_server.py b/examples/transports/mcp_transport_examples/grpc_server.py new file mode 100644 index 0000000000..58933972ae --- /dev/null +++ b/examples/transports/mcp_transport_examples/grpc_server.py @@ -0,0 +1,158 @@ +"""Serve native protobuf RPCs through the SDK's dispatcher interface.""" + +from __future__ import annotations + +import asyncio +from collections.abc import Mapping +from typing import Any, cast + +import anyio +import anyio.abc +import grpc +import grpc.aio +from mcp.shared.dispatcher import CallOptions, OnNotify, OnNotifyIntercept, OnRequest, as_request_id +from mcp.shared.exceptions import MCPError, NoBackChannelError +from mcp.types import INVALID_PARAMS +from pydantic import ValidationError + +from mcp_transport_examples._grpc_codec import decode_json, decode_object, encode_json +from mcp_transport_examples.grpc_context import GRPCContext, GRPCDispatchContext +from mcp_transport_examples.rpc_pb2 import CallEvent, CallRequest, Notification + + +class GRPCServerDispatcher: + """Attach MCP to a borrowed gRPC server without taking ownership of its listener. + + Register before starting the server. The runtime starts this dispatcher; + you start and stop the gRPC server. Each RPC has independent MCP metadata + and a response stream. Shutdown cancels and joins active request handlers. + """ + + def __init__(self, server: grpc.aio.Server, *, max_requests: int = 64) -> None: + if max_requests < 1: + raise ValueError("max_requests must be positive") + self._limit = anyio.CapacityLimiter(max_requests) + self._handler: OnRequest | None = None + self._requests: dict[anyio.CancelScope, anyio.Event] = {} + self._stopped = anyio.Event() + handler = grpc.unary_stream_rpc_method_handler( + self.handle, request_deserializer=CallRequest.FromString, response_serializer=CallEvent.SerializeToString + ) + server.add_generic_rpc_handlers( + [grpc.method_handlers_generic_handler("mcp.transport.example.MCP", {"Call": handler})] + ) + + async def run( + self, + on_request: OnRequest, + on_notify: OnNotify, + on_notify_intercept: OnNotifyIntercept | None = None, + *, + task_status: anyio.abc.TaskStatus[None] = anyio.TASK_STATUS_IGNORED, + ) -> None: + """Install the MCP handler and wait until runtime shutdown.""" + self._handler = on_request + task_status.started() + try: + await self._stopped.wait() + finally: + self._handler = None + self._stopped.set() + requests = tuple(self._requests.items()) + for scope, _ in requests: + scope.cancel() + with anyio.CancelScope(shield=True): + for _, done in requests: + await done.wait() + + async def handle(self, request: CallRequest, context: grpc.aio.ServicerContext[CallRequest, CallEvent]) -> None: + """Handle one gRPC call, with native cancellation and request-scoped notification delivery.""" + handler = self._handler + if handler is None: + await context.abort(grpc.StatusCode.UNAVAILABLE, "MCP dispatcher is not running") + try: + self._limit.acquire_nowait() + except anyio.WouldBlock: + await context.abort(grpc.StatusCode.RESOURCE_EXHAUSTED, "MCP request capacity exhausted") + scope = anyio.CancelScope() + done = anyio.Event() + self._requests[scope] = done + lock = anyio.Lock() + + async def notify(method: str, params: Mapping[str, Any] | None) -> None: + async with lock: + await context.write( + CallEvent(notification=Notification(method=method, params_json=encode_json(params))) + ) + + try: + try: + params = decode_object(request.params_json, nullable=True) + request_id = as_request_id(decode_json(request.request_id_json)) + if request_id is None: + raise ValueError("Invalid request id") + except (ValueError, UnicodeError): + await context.abort(grpc.StatusCode.INVALID_ARGUMENT, "Invalid MCP binding payload") + dctx = GRPCDispatchContext( + transport=GRPCContext( + kind="grpc", + can_send_request=False, + peer=context.peer(), + metadata=cast("tuple[tuple[str, str | bytes], ...]", tuple(context.invocation_metadata() or ())), + peer_identity_key=context.peer_identity_key(), + peer_identities=tuple(context.peer_identities() or ()), + ), + request_id=request_id, + send_notification=notify, + report_progress=request.report_progress, + ) + + response: CallEvent | None = None + ready = anyio.Event() + + async def invoke() -> None: + nonlocal response + try: + result = await handler(dctx, request.method, params) + except MCPError as exc: + response = CallEvent(error_json=encode_json(exc.error.model_dump(by_alias=True))) + except ValidationError: + response = CallEvent( + error_json=encode_json( + {"code": INVALID_PARAMS, "message": "Invalid request parameters", "data": ""} + ) + ) + else: + response = CallEvent(result_json=encode_json(result)) + finally: + ready.set() + + with scope: + async with anyio.create_task_group() as tg: + tg.start_soon(invoke) + try: + await ready.wait() + except asyncio.CancelledError: + if not scope.cancel_called and not tg.cancel_scope.cancel_called: + dctx.cancel_requested.set() + raise + if response is None: + await context.abort(grpc.StatusCode.CANCELLED, "MCP handler ended without a result") + async with lock: + await context.write(response) + if scope.cancelled_caught: + await context.abort(grpc.StatusCode.UNAVAILABLE, "MCP dispatcher closed") + finally: + self._requests.pop(scope) + done.set() + self._limit.release() + + async def send_raw_request( + self, method: str, params: Mapping[str, Any] | None, opts: CallOptions | None = None + ) -> dict[str, Any]: + """Reject server-initiated requests in the modern binding.""" + raise NoBackChannelError(method) + + async def notify(self, method: str, params: Mapping[str, Any] | None, opts: CallOptions | None = None) -> None: + """Reject notifications without an originating RPC; use its DispatchContext instead.""" + raise NoBackChannelError(method) diff --git a/examples/transports/mcp_transport_examples/mqtt.py b/examples/transports/mcp_transport_examples/mqtt.py new file mode 100644 index 0000000000..bb6bc511e1 --- /dev/null +++ b/examples/transports/mcp_transport_examples/mqtt.py @@ -0,0 +1,125 @@ +"""A symmetric MQTT 5 transport for one logical MCP peer.""" + +from __future__ import annotations + +from collections.abc import AsyncIterator +from contextlib import asynccontextmanager, suppress +from dataclasses import dataclass +from types import TracebackType + +import aiomqtt +import anyio +from mcp.shared.transport import SessionMessage, TransportStreams +from mcp.types import jsonrpc_message_adapter +from paho.mqtt.packettypes import PacketTypes +from paho.mqtt.properties import Properties +from paho.mqtt.subscribeoptions import SubscribeOptions +from pydantic import ValidationError +from typing_extensions import Self + + +@asynccontextmanager +async def mqtt_transport( + client: aiomqtt.Client, + *, + incoming_topic: str, + outgoing_topic: str, + expiry: int = 60, + max_message_size: int = 4 * 1024 * 1024, +) -> AsyncIterator[TransportStreams]: + """Connect one peer over two dedicated MQTT 5 topics using QoS 2. + + You own and enter `client`. Give each connection fresh topics, grant only + its peer access through broker ACLs, and dedicate the client's messages + iterator to this transport. Empty payloads close the logical connection. + Retained messages are rejected; this adapter never reconnects or replays. + + Args: + client: An entered MQTT 5 client with a bounded incoming queue. + incoming_topic: Exact topic to receive from, without wildcards. + outgoing_topic: Exact topic to publish to, without wildcards. + expiry: Broker expiry for messages and the close signal, in seconds. + max_message_size: Maximum encoded message size in either direction. + + Raises: + ValueError: If the configuration is invalid or an outgoing message is too large. + aiomqtt.MqttError: If subscription or publication fails. + """ + aiomqtt.Topic(incoming_topic) + aiomqtt.Topic(outgoing_topic) + if incoming_topic == outgoing_topic: + raise ValueError("MQTT directions must use different topics") + if not 0 < expiry <= 2**32 - 1 or max_message_size < 1: + raise ValueError("expiry must be a positive uint32 and max_message_size must be positive") + properties = Properties(PacketTypes.PUBLISH) + properties.MessageExpiryInterval = expiry + writer = _MQTTWriter(client, outgoing_topic, properties, max_message_size) + send, receive = anyio.create_memory_object_stream[SessionMessage | Exception](0) + + async def read_messages() -> None: + async with send: + try: + async for message in client.messages: + if str(message.topic) != incoming_topic: + continue + if message.retain or message.qos != 2 or len(message.payload) > max_message_size: + await send.send(ValueError("Rejected retained, non-QoS-2, or oversized MQTT message")) + continue + if not message.payload: + break + try: + decoded = jsonrpc_message_adapter.validate_json(message.payload, by_name=False) + except ValidationError as exc: + await send.send(exc) + else: + await send.send(SessionMessage(decoded)) + except (aiomqtt.MqttError, anyio.BrokenResourceError, anyio.ClosedResourceError): + pass + + try: + await client.subscribe( + incoming_topic, options=SubscribeOptions(qos=2, retainAsPublished=True, retainHandling=2) + ) + async with receive, writer: + async with anyio.create_task_group() as tg: + tg.start_soon(read_messages) + try: + yield receive, writer + finally: + tg.cancel_scope.cancel() + finally: + await send.aclose() + await receive.aclose() + with anyio.move_on_after(1, shield=True), suppress(aiomqtt.MqttError): + await client.unsubscribe(incoming_topic) + + +@dataclass +class _MQTTWriter: + client: aiomqtt.Client + topic: str + properties: Properties + max_message_size: int + closed: bool = False + + async def send(self, item: SessionMessage, /) -> None: + if self.closed: + raise anyio.ClosedResourceError + payload = item.message.model_dump_json(by_alias=True, exclude_none=True).encode() + if len(payload) > self.max_message_size: + raise ValueError("Encoded MCP message exceeds max_message_size") + await self.client.publish(self.topic, payload, qos=2, retain=False, properties=self.properties) + + async def aclose(self) -> None: + if not self.closed: + self.closed = True + with anyio.move_on_after(1, shield=True), suppress(aiomqtt.MqttError): + await self.client.publish(self.topic, b"", qos=2, retain=False, properties=self.properties) + + async def __aenter__(self) -> Self: + return self + + async def __aexit__( + self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None + ) -> None: + await self.aclose() diff --git a/examples/transports/mcp_transport_examples/py.typed b/examples/transports/mcp_transport_examples/py.typed new file mode 100644 index 0000000000..e69de29bb2 diff --git a/examples/transports/mcp_transport_examples/rpc.proto b/examples/transports/mcp_transport_examples/rpc.proto new file mode 100644 index 0000000000..9f660545f0 --- /dev/null +++ b/examples/transports/mcp_transport_examples/rpc.proto @@ -0,0 +1,27 @@ +syntax = "proto3"; + +package mcp.transport.example; + +service MCP { + rpc Call(CallRequest) returns (stream CallEvent); +} + +message CallRequest { + string method = 1; + bytes params_json = 2; + bytes request_id_json = 3; + bool report_progress = 4; +} + +message CallEvent { + oneof payload { + bytes result_json = 1; + bytes error_json = 2; + Notification notification = 3; + } +} + +message Notification { + string method = 1; + bytes params_json = 2; +} diff --git a/examples/transports/mcp_transport_examples/rpc_pb2.py b/examples/transports/mcp_transport_examples/rpc_pb2.py new file mode 100644 index 0000000000..c2d2b563a5 --- /dev/null +++ b/examples/transports/mcp_transport_examples/rpc_pb2.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: mcp_transport_examples/rpc.proto +# Protobuf Python Version: 6.33.5 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 6, + 33, + 5, + '', + 'mcp_transport_examples/rpc.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n mcp_transport_examples/rpc.proto\x12\x15mcp.transport.example\"d\n\x0b\x43\x61llRequest\x12\x0e\n\x06method\x18\x01 \x01(\t\x12\x13\n\x0bparams_json\x18\x02 \x01(\x0c\x12\x17\n\x0frequest_id_json\x18\x03 \x01(\x0c\x12\x17\n\x0freport_progress\x18\x04 \x01(\x08\"\x80\x01\n\tCallEvent\x12\x15\n\x0bresult_json\x18\x01 \x01(\x0cH\x00\x12\x14\n\nerror_json\x18\x02 \x01(\x0cH\x00\x12;\n\x0cnotification\x18\x03 \x01(\x0b\x32#.mcp.transport.example.NotificationH\x00\x42\t\n\x07payload\"3\n\x0cNotification\x12\x0e\n\x06method\x18\x01 \x01(\t\x12\x13\n\x0bparams_json\x18\x02 \x01(\x0c\x32U\n\x03MCP\x12N\n\x04\x43\x61ll\x12\".mcp.transport.example.CallRequest\x1a .mcp.transport.example.CallEvent0\x01\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'mcp_transport_examples.rpc_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + DESCRIPTOR._loaded_options = None + _globals['_CALLREQUEST']._serialized_start=59 + _globals['_CALLREQUEST']._serialized_end=159 + _globals['_CALLEVENT']._serialized_start=162 + _globals['_CALLEVENT']._serialized_end=290 + _globals['_NOTIFICATION']._serialized_start=292 + _globals['_NOTIFICATION']._serialized_end=343 + _globals['_MCP']._serialized_start=345 + _globals['_MCP']._serialized_end=430 +# @@protoc_insertion_point(module_scope) diff --git a/examples/transports/mcp_transport_examples/rpc_pb2.pyi b/examples/transports/mcp_transport_examples/rpc_pb2.pyi new file mode 100644 index 0000000000..cc41d17add --- /dev/null +++ b/examples/transports/mcp_transport_examples/rpc_pb2.pyi @@ -0,0 +1,36 @@ +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from collections.abc import Mapping as _Mapping +from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union + +DESCRIPTOR: _descriptor.FileDescriptor + +class CallRequest(_message.Message): + __slots__ = ("method", "params_json", "request_id_json", "report_progress") + METHOD_FIELD_NUMBER: _ClassVar[int] + PARAMS_JSON_FIELD_NUMBER: _ClassVar[int] + REQUEST_ID_JSON_FIELD_NUMBER: _ClassVar[int] + REPORT_PROGRESS_FIELD_NUMBER: _ClassVar[int] + method: str + params_json: bytes + request_id_json: bytes + report_progress: bool + def __init__(self, method: _Optional[str] = ..., params_json: _Optional[bytes] = ..., request_id_json: _Optional[bytes] = ..., report_progress: _Optional[bool] = ...) -> None: ... + +class CallEvent(_message.Message): + __slots__ = ("result_json", "error_json", "notification") + RESULT_JSON_FIELD_NUMBER: _ClassVar[int] + ERROR_JSON_FIELD_NUMBER: _ClassVar[int] + NOTIFICATION_FIELD_NUMBER: _ClassVar[int] + result_json: bytes + error_json: bytes + notification: Notification + def __init__(self, result_json: _Optional[bytes] = ..., error_json: _Optional[bytes] = ..., notification: _Optional[_Union[Notification, _Mapping]] = ...) -> None: ... + +class Notification(_message.Message): + __slots__ = ("method", "params_json") + METHOD_FIELD_NUMBER: _ClassVar[int] + PARAMS_JSON_FIELD_NUMBER: _ClassVar[int] + method: str + params_json: bytes + def __init__(self, method: _Optional[str] = ..., params_json: _Optional[bytes] = ...) -> None: ... diff --git a/examples/transports/pyproject.toml b/examples/transports/pyproject.toml new file mode 100644 index 0000000000..6211c54352 --- /dev/null +++ b/examples/transports/pyproject.toml @@ -0,0 +1,68 @@ +[project] +name = "mcp-transport-examples" +version = "0.1.0" +description = "Reference MQTT, AMQP, and native gRPC adapters for the MCP transport API" +requires-python = ">=3.10" +dependencies = [ + "aio-pika>=9.5", + "aiomqtt>=2.4", + "grpcio>=1.71", + "mcp", + "protobuf>=6.33.5", +] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["mcp_transport_examples"] + +[dependency-groups] +dev = [ + "pytest>=8.4.0", + "coverage[toml]>=7.10.7", + "pyright>=1.1.400", + "ruff>=0.8.5", + "grpcio-tools==1.81.1", + "types-protobuf>=7.35.1.20260906", + "cassetter[grpc]>=0.11.0", + "cryptography>=50.0.0", +] + +[tool.pytest.ini_options] +addopts = "--strict-config --strict-markers" +testpaths = ["tests"] +filterwarnings = ["error"] +xfail_strict = true + +[tool.coverage.run] +branch = true +source_pkgs = ["mcp_transport_examples", "tests"] +# Protoc output is verified by regenerating it, not by testing protobuf internals. +omit = ["*/rpc_pb2.py"] + +[tool.coverage.report] +fail_under = 100 +show_missing = true +exclude_also = ["if TYPE_CHECKING:", "raise NotImplementedError", "@overload"] + +[tool.pyright] +typeCheckingMode = "strict" +include = ["mcp_transport_examples", "tests", "*.py"] +# Protoc emits unparameterized Mapping annotations in its generated stubs. +ignore = ["mcp_transport_examples/rpc_pb2.py", "mcp_transport_examples/rpc_pb2.pyi"] +venvPath = "." +venv = ".venv" +reportUnusedFunction = false + +[tool.ruff] +line-length = 120 +target-version = "py310" +extend-exclude = ["rpc_pb2.py", "rpc_pb2.pyi"] + +[tool.ruff.lint] +select = ["E", "F", "I", "FA", "UP", "RUF100"] + +[tool.ruff.lint.isort] +combine-as-imports = true diff --git a/examples/transports/reproduce_grpc_loop_shutdown.py b/examples/transports/reproduce_grpc_loop_shutdown.py new file mode 100644 index 0000000000..2307e97e18 --- /dev/null +++ b/examples/transports/reproduce_grpc_loop_shutdown.py @@ -0,0 +1,37 @@ +"""Reproduce late gRPC connectivity completions without importing the MCP SDK.""" + +import asyncio +import sys + +import anyio +import anyio.abc +import grpc +import grpc.aio + + +def main() -> None: + """Exit unsuccessfully when a native completion targets an earlier, closed loop.""" + channels: list[grpc.aio.Channel] = [] + errors: list[dict[str, object]] = [] + + async def run() -> None: + asyncio.get_running_loop().set_exception_handler(lambda loop, context: errors.append(context)) + channel = grpc.aio.insecure_channel("127.0.0.1:1") + channels.append(channel) + + async def watch(*, task_status: anyio.abc.TaskStatus[None] = anyio.TASK_STATUS_IGNORED) -> None: + task_status.started() + await channel.wait_for_state_change(channel.get_state()) + + async with anyio.create_task_group() as tg: + await tg.start(watch) + tg.cancel_scope.cancel() + await channel.close() + + for _ in range(10): + anyio.run(run) + assert not errors, (grpc.__version__, sys.version, errors) + + +if __name__ == "__main__": + main() diff --git a/examples/transports/tests/__init__.py b/examples/transports/tests/__init__.py new file mode 100644 index 0000000000..a9a2c5b3bb --- /dev/null +++ b/examples/transports/tests/__init__.py @@ -0,0 +1 @@ +__all__ = [] diff --git a/examples/transports/tests/cassettes/test_grpc/test_native_error_keeps_code_message_and_data.yaml b/examples/transports/tests/cassettes/test_grpc/test_native_error_keeps_code_message_and_data.yaml new file mode 100644 index 0000000000..11fd028a43 --- /dev/null +++ b/examples/transports/tests/cassettes/test_grpc/test_native_error_keeps_code_message_and_data.yaml @@ -0,0 +1,19 @@ +version: 1 +interactions: [] +grpc_interactions: + - request: + method: /mcp.transport.example.MCP/Call + metadata: {} + body: + type: binary + content: >- + 0a0e6578616d706c652f72656675736512b8017b225f6d657461223a7b22696f2e6d6f64656c636f6e7465787470726f746f636f6c2f70726f746f636f6c56657273696f6e223a22323032362d30372d3238222c22696f2e6d6f64656c636f6e7465787470726f746f636f6c2f636c69656e74496e666f223a7b226e616d65223a226d6370222c2276657273696f6e223a22302e312e30227d2c22696f2e6d6f64656c636f6e7465787470726f746f636f6c2f636c69656e744361706162696c6974696573223a7b7d7d7d1a0130 + response: + status_code: 0 + status_message: OK + metadata: {} + body: + type: binary + content: >- + 0000007912777b22636f6465223a2d313039393531313632373737362c226d657373616765223a226170706c69636174696f6e207265667573616c222c2264617461223a7b2276656e646f722f726561736f6e223a226361706163697479222c226c61726765223a393232333337323033363835343737353830397d7d + recorded_at: 2026-09-16T16:13:04.953959+00:00 diff --git a/examples/transports/tests/cassettes/test_grpc/test_native_payload_keeps_large_integers_and_extension_fields.yaml b/examples/transports/tests/cassettes/test_grpc/test_native_payload_keeps_large_integers_and_extension_fields.yaml new file mode 100644 index 0000000000..ef9d397814 --- /dev/null +++ b/examples/transports/tests/cassettes/test_grpc/test_native_payload_keeps_large_integers_and_extension_fields.yaml @@ -0,0 +1,19 @@ +version: 1 +interactions: [] +grpc_interactions: + - request: + method: /mcp.transport.example.MCP/Call + metadata: {} + body: + type: binary + content: >- + 0a0c6578616d706c652f6563686f128a027b2276616c7565223a7b226c61726765223a393232333337323033363835343737353830392c2276656e646f722f6669656c64223a5b6e756c6c2c7b226c6162656c223a226361665c7530306539227d5d7d2c225f6d657461223a7b22696f2e6d6f64656c636f6e7465787470726f746f636f6c2f70726f746f636f6c56657273696f6e223a22323032362d30372d3238222c22696f2e6d6f64656c636f6e7465787470726f746f636f6c2f636c69656e74496e666f223a7b226e616d65223a226d6370222c2276657273696f6e223a22302e312e30227d2c22696f2e6d6f64656c636f6e7465787470726f746f636f6c2f636c69656e744361706162696c6974696573223a7b7d7d7d1a0130 + response: + status_code: 0 + status_message: OK + metadata: {} + body: + type: binary + content: >- + 000000bc0ab9017b2276616c7565223a7b226c61726765223a393232333337323033363835343737353830392c2276656e646f722f6669656c64223a5b6e756c6c2c7b226c6162656c223a226361665c7530306539227d5d7d2c22726573756c7454797065223a22636f6d706c657465222c225f6d657461223a7b22696f2e6d6f64656c636f6e7465787470726f746f636f6c2f736572766572496e666f223a7b226e616d65223a226e6174697665222c2276657273696f6e223a22227d7d7d + recorded_at: 2026-09-16T16:13:04.939254+00:00 diff --git a/examples/transports/tests/cassettes/test_grpc/test_native_progress_reaches_the_client_before_the_result.yaml b/examples/transports/tests/cassettes/test_grpc/test_native_progress_reaches_the_client_before_the_result.yaml new file mode 100644 index 0000000000..2b57775b97 --- /dev/null +++ b/examples/transports/tests/cassettes/test_grpc/test_native_progress_reaches_the_client_before_the_result.yaml @@ -0,0 +1,19 @@ +version: 1 +interactions: [] +grpc_interactions: + - request: + method: /mcp.transport.example.MCP/Call + metadata: {} + body: + type: binary + content: >- + 0a0a746f6f6c732f63616c6c12ee017b226e616d65223a226563686f222c22617267756d656e7473223a7b2276616c7565223a226e61746976652070726f6772657373227d2c225f6d657461223a7b22696f2e6d6f64656c636f6e7465787470726f746f636f6c2f70726f746f636f6c56657273696f6e223a22323032362d30372d3238222c22696f2e6d6f64656c636f6e7465787470726f746f636f6c2f636c69656e74496e666f223a7b226e616d65223a226d6370222c2276657273696f6e223a22302e312e30227d2c22696f2e6d6f64656c636f6e7465787470726f746f636f6c2f636c69656e744361706162696c6974696573223a7b7d7d7d1a01302001 + response: + status_code: 0 + status_message: OK + metadata: {} + body: + type: binary + content: >- + 0000005a1a580a166e6f74696669636174696f6e732f70726f6772657373123e7b2270726f6772657373546f6b656e223a302c2270726f6772657373223a312c22746f74616c223a322c226d657373616765223a2268616c66776179227d000000e90ae6017b22636f6e74656e74223a5b7b2274657874223a226e61746976652070726f6772657373222c2274797065223a2274657874227d5d2c2269734572726f72223a66616c73652c22726573756c7454797065223a22636f6d706c657465222c2273747275637475726564436f6e74656e74223a7b22726573756c74223a226e61746976652070726f6772657373227d2c225f6d657461223a7b22696f2e6d6f64656c636f6e7465787470726f746f636f6c2f736572766572496e666f223a7b226e616d65223a226e61746976652d70726f6772657373222c2276657273696f6e223a22227d7d7d + recorded_at: 2026-09-16T16:13:04.948671+00:00 diff --git a/examples/transports/tests/conftest.py b/examples/transports/tests/conftest.py new file mode 100644 index 0000000000..c4f226d6db --- /dev/null +++ b/examples/transports/tests/conftest.py @@ -0,0 +1,14 @@ +from collections.abc import AsyncIterator + +import pytest + + +@pytest.fixture(scope="session") +def anyio_backend() -> str: + return "asyncio" + + +@pytest.fixture(scope="session", autouse=True) +async def grpc_event_loop(anyio_backend: str) -> AsyncIterator[None]: + """Keep gRPC's process-wide completion queue on one loop, including late connectivity callbacks.""" + yield diff --git a/examples/transports/tests/test_amqp.py b/examples/transports/tests/test_amqp.py new file mode 100644 index 0000000000..11b0a346e5 --- /dev/null +++ b/examples/transports/tests/test_amqp.py @@ -0,0 +1,30 @@ +import pytest +from aio_pika import Channel, Connection +from yarl import URL + +from mcp_transport_examples.amqp import amqp_transport + + +@pytest.mark.anyio +@pytest.mark.parametrize( + ("incoming", "outgoing", "expiry", "size", "confirms"), + [ + ("", "responses", 60, 4096, True), + ("requests", "", 60, 4096, True), + ("same", "same", 60, 4096, True), + ("requests", "responses", 0, 4096, True), + ("requests", "responses", 60, 0, True), + ("requests", "responses", 60, 4096, False), + ], +) +async def test_invalid_configuration_fails_without_connecting( + incoming: str, outgoing: str, expiry: int, size: int, confirms: bool +) -> None: + """Adapter-defined constraints reject unsafe queue routing and limits before touching a broker.""" + connection = Connection(URL("amqp://unused.invalid")) + channel = Channel(connection, publisher_confirms=confirms) + with pytest.raises(ValueError): + async with amqp_transport( + channel, incoming_queue=incoming, outgoing_queue=outgoing, expiry=expiry, max_message_size=size + ): + raise NotImplementedError diff --git a/examples/transports/tests/test_grpc.py b/examples/transports/tests/test_grpc.py new file mode 100644 index 0000000000..972f01b22c --- /dev/null +++ b/examples/transports/tests/test_grpc.py @@ -0,0 +1,185 @@ +import json +from collections.abc import AsyncIterator, Callable +from contextlib import AsyncExitStack, asynccontextmanager +from typing import Any + +import anyio +import grpc.aio +import pytest +from cassetter import Cassette, Cassetter +from mcp import Client, MCPError +from mcp.server import Server, ServerRequestContext +from mcp.server.mcpserver import Context, MCPServer +from mcp.types import ( + CONNECTION_CLOSED, + CallToolRequest, + CallToolRequestParams, + CallToolResult, + Request, + RequestParams, + Result, +) + +from mcp_transport_examples.grpc import grpc_client, grpc_server +from mcp_transport_examples.rpc_pb2 import CallEvent, CallRequest + + +@pytest.fixture(scope="module") +def vcr_config() -> Cassetter: + return Cassetter(intercept=["grpc"]) + + +@asynccontextmanager +async def connected( + server: Server[Any] | MCPServer[Any], cassette: Cassette, monkeypatch: pytest.MonkeyPatch +) -> AsyncIterator[Client]: + async with AsyncExitStack() as stack: + listener = grpc.aio.server() + port = listener.add_insecure_port("127.0.0.1:0") + stack.push_async_callback(listener.stop, 0) + runtime = await stack.enter_async_context(server.serve()) + await runtime.connect(grpc_server(listener)) + await listener.start() + channel = await stack.enter_async_context(grpc.aio.insecure_channel(f"127.0.0.1:{port}")) + requests: list[CallRequest] = [] + unary_stream = channel.unary_stream + + def capture( + method: str, + request_serializer: Callable[[CallRequest], bytes] | None = None, + response_deserializer: Callable[[bytes], CallEvent] | None = None, + ) -> grpc.aio.UnaryStreamMultiCallable[CallRequest, CallEvent]: + assert request_serializer is not None + + def serialize(request: CallRequest) -> bytes: + payload = request_serializer(request) + requests.append(CallRequest.FromString(payload)) + return payload + + return unary_stream(method, request_serializer=serialize, response_deserializer=response_deserializer) + + monkeypatch.setattr(channel, "unary_stream", capture) + client = await stack.enter_async_context(Client(grpc_client(channel), mode="2026-07-28")) + yield client + assert requests + assert len(cassette.grpc_interactions) == 1 + payload = cassette.grpc_interactions[0].request.body.content + assert isinstance(payload, bytes) + recorded = CallRequest.FromString(payload) + for request in requests: + # cassetter currently matches gRPC methods, not request bodies. + assert request.method == recorded.method + assert json.loads(request.params_json) == json.loads(recorded.params_json) + assert request.request_id_json == recorded.request_id_json + assert request.report_progress == recorded.report_progress + + +@pytest.mark.anyio +@pytest.mark.vcr +async def test_native_payload_keeps_large_integers_and_extension_fields( + cassette: Cassette, monkeypatch: pytest.MonkeyPatch +) -> None: + """A recorded real RPC preserves arbitrary MCP payload fields without protobuf Struct's float conversion.""" + + class EchoParams(RequestParams): + value: dict[str, Any] + + class EchoResult(Result): + value: dict[str, Any] + + async def echo(ctx: ServerRequestContext, params: EchoParams) -> EchoResult: + assert ctx.method == "example/echo" + return EchoResult(value=params.value) + + server = Server("native") + server.add_request_handler("example/echo", EchoParams, echo) + payload = {"large": 2**63 + 1, "vendor/field": [None, {"label": "café"}]} + with anyio.fail_after(5): + async with connected(server, cassette, monkeypatch) as client: + result = await client.session.send_request( + Request(method="example/echo", params=EchoParams(value=payload)), EchoResult + ) + assert result.value == payload + async with Client(server, mode="2026-07-28") as local: + expected = await local.session.send_request( + Request(method="example/echo", params=EchoParams(value=payload)), EchoResult + ) + assert result == expected + + +@pytest.mark.anyio +@pytest.mark.vcr +async def test_native_progress_reaches_the_client_before_the_result( + cassette: Cassette, monkeypatch: pytest.MonkeyPatch +) -> None: + """A recorded response stream routes progress through the SDK callback, isolated from tools/list schema fetching.""" + server = MCPServer("native-progress") + + @server.tool() + async def echo(value: str, ctx: Context) -> str: + await ctx.report_progress(1, 2, "halfway") + return value + + updates: list[tuple[float, float | None, str | None]] = [] + + async def progress(progress: float, total: float | None, message: str | None) -> None: + updates.append((progress, total, message)) + + value = "native progress" + with anyio.fail_after(5): + async with connected(server, cassette, monkeypatch) as client: + result = await client.session.send_request( + CallToolRequest(params=CallToolRequestParams(name="echo", arguments={"value": value})), + CallToolResult, + progress_callback=progress, + ) + assert result.structured_content == {"result": value} + wire_updates = updates.copy() + updates.clear() + async with Client(server, mode="2026-07-28") as local: + expected = await local.session.send_request( + CallToolRequest(params=CallToolRequestParams(name="echo", arguments={"value": value})), + CallToolResult, + progress_callback=progress, + ) + assert result == expected + assert updates == wire_updates == [(1, 2, "halfway")] + + +@pytest.mark.anyio +async def test_request_immediately_after_channel_close_reports_mcp_connection_closed() -> None: + """An idle borrowed channel closes without a network request or a scheduling opportunity for its watcher.""" + with anyio.fail_after(5): + async with grpc.aio.insecure_channel("unused.invalid:50051") as channel: + async with Client(grpc_client(channel), mode="2026-07-28") as client: + await channel.close() + with pytest.raises(MCPError) as exc: + await client.list_tools() + assert exc.value.code == CONNECTION_CLOSED + + +@pytest.mark.anyio +@pytest.mark.vcr +async def test_native_error_keeps_code_message_and_data(cassette: Cassette, monkeypatch: pytest.MonkeyPatch) -> None: + """Native application errors retain all MCP fields, including codes outside signed int32.""" + code = -(2**40) + message = "application refusal" + data = {"vendor/reason": "capacity", "large": 2**63 + 1} + + async def refuse(ctx: ServerRequestContext, params: RequestParams) -> Result: + assert ctx.method == "example/refuse" + raise MCPError(code=code, message=message, data=data) + + server = Server("native-errors") + server.add_request_handler("example/refuse", RequestParams, refuse) + with anyio.fail_after(5): + async with connected(server, cassette, monkeypatch) as client: + with pytest.raises(MCPError) as exc: + await client.session.send_request(Request(method="example/refuse", params=RequestParams()), Result) + assert exc.value.code == code + assert exc.value.message == message + assert exc.value.data == data + async with Client(server, mode="2026-07-28") as local: + with pytest.raises(MCPError) as expected: + await local.session.send_request(Request(method="example/refuse", params=RequestParams()), Result) + assert exc.value.error == expected.value.error diff --git a/examples/transports/tests/test_grpc_cancel_signal.py b/examples/transports/tests/test_grpc_cancel_signal.py new file mode 100644 index 0000000000..2bc9e9e6e9 --- /dev/null +++ b/examples/transports/tests/test_grpc_cancel_signal.py @@ -0,0 +1,72 @@ +"""Peer cancellation must be visible before the handler's cleanup runs.""" + +from collections.abc import Mapping +from typing import Any + +import anyio +import anyio.abc +import grpc.aio +import pytest +from mcp import Client +from mcp.shared.dispatcher import DispatchContext +from mcp.shared.transport import TransportContext +from mcp.types import Request, RequestParams, Result + +from mcp_transport_examples.grpc import grpc_client +from mcp_transport_examples.grpc_server import GRPCServerDispatcher + + +async def verify() -> None: + entered = anyio.Event() + done = anyio.Event() + observed: list[bool] = [] + + async def handle( + ctx: DispatchContext[TransportContext], method: str, params: Mapping[str, Any] | None + ) -> dict[str, Any]: + assert method == "example/wait" + entered.set() + try: + await anyio.sleep_forever() + finally: + observed.append(ctx.cancel_requested.is_set()) + done.set() + raise NotImplementedError + + async def notify(ctx: DispatchContext[TransportContext], method: str, params: Mapping[str, Any] | None) -> None: + raise NotImplementedError + + listener = grpc.aio.server() + port = listener.add_insecure_port("127.0.0.1:0") + dispatcher = GRPCServerDispatcher(listener) + try: + async with anyio.create_task_group() as tg: + await tg.start(dispatcher.run, handle, notify) + await listener.start() + async with grpc.aio.insecure_channel(f"127.0.0.1:{port}") as channel: + async with Client(grpc_client(channel), mode="2026-07-28") as client: + + async def call(*, task_status: anyio.abc.TaskStatus[anyio.CancelScope]) -> None: + with anyio.CancelScope() as scope: + task_status.started(scope) + await client.session.send_request( + Request(method="example/wait", params=RequestParams()), Result + ) + + async with anyio.create_task_group() as calls: + scope = await calls.start(call) + await entered.wait() + scope.cancel() + await done.wait() + assert observed == [True] + tg.cancel_scope.cancel() + finally: + with anyio.move_on_after(5, shield=True): + await listener.stop(0) + + +@pytest.mark.anyio +async def test_peer_cancellation_is_signalled_before_handler_cleanup() -> None: + """Read cancel_requested during the live handler's cleanup, not after RPC completion.""" + with anyio.fail_after(5): + await verify() diff --git a/examples/transports/tests/test_grpc_client.py b/examples/transports/tests/test_grpc_client.py new file mode 100644 index 0000000000..2148658be9 --- /dev/null +++ b/examples/transports/tests/test_grpc_client.py @@ -0,0 +1,106 @@ +from contextlib import AsyncExitStack +from typing import Any + +import anyio +import grpc.aio +import pytest +from mcp.client import ClientSession +from mcp.server.mcpserver import Context, MCPServer +from mcp.shared.exceptions import MCPError, NoBackChannelError +from mcp.types import CLIENT_CAPABILITIES_META_KEY, CONNECTION_CLOSED, PROTOCOL_VERSION_META_KEY, CallToolRequestParams + +from mcp_transport_examples.grpc import grpc_client, grpc_server + + +@pytest.mark.anyio +@pytest.mark.parametrize("closed_channel", [False, True], ids=["session-exit", "closed-channel-startup"]) +async def test_unstarted_and_closed_dispatchers_never_issue_an_rpc(closed_channel: bool) -> None: + """Public dispatcher guards reject requests before startup and drop notifications after closure without dialing.""" + with anyio.fail_after(5): + async with ( + grpc.aio.insecure_channel("unused.invalid:50051") as channel, + grpc_client(channel).connection as dispatcher, + ): + with pytest.raises(RuntimeError): + await dispatcher.send_raw_request("example/test", None) + with pytest.raises(NoBackChannelError): + await dispatcher.notify("example/event", None) + if closed_channel: + await channel.close() + async with ClientSession(dispatcher=dispatcher): + pass + await dispatcher.notify("example/event", None) + with pytest.raises(MCPError) as exc: + await dispatcher.send_raw_request("example/test", None) + assert exc.value.code == CONNECTION_CLOSED + + +@pytest.mark.anyio +@pytest.mark.parametrize("value", ["a" * (4 * 1024 * 1024), float("nan")], ids=["oversized", "nonfinite"]) +async def test_invalid_outgoing_payload_fails_before_dialing(value: str | float) -> None: + """The raw dispatcher rejects invalid JSON; the typed client normalizes nonfinite values before this boundary.""" + with anyio.fail_after(5): + async with ( + grpc.aio.insecure_channel("unused.invalid:50051") as channel, + grpc_client(channel).connection as dispatcher, + ClientSession(dispatcher=dispatcher), + ): + with pytest.raises(ValueError): + await dispatcher.send_raw_request("example/test", {"value": value}) + assert channel.get_state() == grpc.ChannelConnectivity.IDLE + + +@pytest.mark.anyio +async def test_request_ids_preserve_spelling_and_reject_in_flight_collisions() -> None: + """Exercise the public dispatcher option that the high-level client normally supplies for subscriptions.""" + entered = anyio.Event() + release = anyio.Event() + server = MCPServer("request IDs") + + @server.tool() + async def identify(hold: bool, ctx: Context) -> str | int | None: + if hold: + entered.set() + await release.wait() + return ctx.request_context.request_id + + waiting = CallToolRequestParams( + name="identify", + arguments={"hold": True}, + _meta={PROTOCOL_VERSION_META_KEY: "2026-07-28", CLIENT_CAPABILITIES_META_KEY: {}}, + ).model_dump(by_alias=True, exclude_none=True) + immediate = CallToolRequestParams( + name="identify", + arguments={"hold": False}, + _meta={PROTOCOL_VERSION_META_KEY: "2026-07-28", CLIENT_CAPABILITIES_META_KEY: {}}, + ).model_dump(by_alias=True, exclude_none=True) + results: list[dict[str, Any]] = [] + + with anyio.fail_after(5): + async with AsyncExitStack() as stack: + listener = grpc.aio.server() + port = listener.add_insecure_port("127.0.0.1:0") + stack.push_async_callback(listener.stop, 0) + runtime = await stack.enter_async_context(server.serve()) + await runtime.connect(grpc_server(listener)) + await listener.start() + channel = await stack.enter_async_context(grpc.aio.insecure_channel(f"127.0.0.1:{port}")) + dispatcher = await stack.enter_async_context(grpc_client(channel).connection) + await stack.enter_async_context(ClientSession(dispatcher=dispatcher)) + + async def first() -> None: + results.append(await dispatcher.send_raw_request("tools/call", waiting, {"request_id": 0})) + + async with anyio.create_task_group() as tg: + tg.start_soon(first) + try: + await entered.wait() + with pytest.raises(ValueError): + await dispatcher.send_raw_request("tools/call", immediate, {"request_id": "0"}) + minted = await dispatcher.send_raw_request("tools/call", immediate) + assert minted["structuredContent"] == {"result": 1} + finally: + release.set() + assert results[0]["structuredContent"] == {"result": 0} + reused = await dispatcher.send_raw_request("tools/call", immediate, {"request_id": "0"}) + assert reused["structuredContent"] == {"result": "0"} diff --git a/examples/transports/tests/test_grpc_client_shutdown.py b/examples/transports/tests/test_grpc_client_shutdown.py new file mode 100644 index 0000000000..bf941c0540 --- /dev/null +++ b/examples/transports/tests/test_grpc_client_shutdown.py @@ -0,0 +1,94 @@ +"""Client shutdown must interrupt callbacks as well as gRPC socket reads.""" + +import anyio +import anyio.abc +import grpc.aio +import pytest +from mcp import Client, MCPError +from mcp.server.mcpserver import Context, MCPServer +from mcp.types import CONNECTION_CLOSED + +from mcp_transport_examples.grpc import grpc_client, grpc_server + + +async def verify(*, shield_cleanup: bool = False) -> None: + cleanup_started = anyio.Event() + release_cleanup = anyio.Event() + callback_entered = anyio.Event() + callback_cancelled = anyio.Event() + close_client = anyio.Event() + client_closed = anyio.Event() + call_finished = anyio.Event() + server_cancelled = anyio.Event() + server = MCPServer("client shutdown") + + @server.tool() + async def wait(ctx: Context) -> str: + try: + await ctx.report_progress(1, 2) + await anyio.sleep_forever() + finally: + server_cancelled.set() + raise NotImplementedError + + async def progress(progress: float, total: float | None, message: str | None) -> None: + callback_entered.set() + try: + await anyio.sleep_forever() + finally: + if shield_cleanup: + with anyio.CancelScope(shield=True): + cleanup_started.set() + await release_cleanup.wait() + callback_cancelled.set() + + listener = grpc.aio.server() + port = listener.add_insecure_port("127.0.0.1:0") + try: + async with server.serve() as runtime: + await runtime.connect(grpc_server(listener)) + await listener.start() + async with grpc.aio.insecure_channel(f"127.0.0.1:{port}") as channel: + + async def own_client(*, task_status: anyio.abc.TaskStatus[Client]) -> None: + async with Client(grpc_client(channel), mode="2026-07-28") as client: + task_status.started(client) + await close_client.wait() + client_closed.set() + + async def call(client: Client) -> None: + with pytest.raises(MCPError) as exc: + await client.call_tool("wait", progress_callback=progress) + assert exc.value.code == CONNECTION_CLOSED + call_finished.set() + + async with anyio.create_task_group() as tg: + client = await tg.start(own_client) + tg.start_soon(call, client) + try: + await callback_entered.wait() + close_client.set() + if shield_cleanup: + await cleanup_started.wait() + # Shutdown must wait for this callback, not abandon it after five seconds. + with anyio.move_on_after(5.1) as window: + await client_closed.wait() + assert window.cancelled_caught + finally: + release_cleanup.set() + await client_closed.wait() + await call_finished.wait() + await server_cancelled.wait() + assert callback_cancelled.is_set() + finally: + with anyio.move_on_after(5, shield=True): + await listener.stop(0) + + +@pytest.mark.anyio +@pytest.mark.parametrize("shield_cleanup", [False, True]) +async def test_client_shutdown_joins_blocked_callbacks(shield_cleanup: bool) -> None: + """The client must wait for callback cleanup before relinquishing its session resources.""" + # The shielded case deliberately exceeds the former five-second join deadline. + with anyio.fail_after(10): + await verify(shield_cleanup=shield_cleanup) diff --git a/examples/transports/tests/test_grpc_context.py b/examples/transports/tests/test_grpc_context.py new file mode 100644 index 0000000000..9fba9c4dc4 --- /dev/null +++ b/examples/transports/tests/test_grpc_context.py @@ -0,0 +1,51 @@ +from collections.abc import Mapping +from typing import Any + +import anyio +import grpc.aio +import pytest +from mcp.shared.exceptions import NoBackChannelError + +from mcp_transport_examples.grpc import grpc_server +from mcp_transport_examples.grpc_context import GRPCContext, GRPCDispatchContext + + +@pytest.mark.anyio +async def test_modern_binding_refuses_server_requests_and_unscoped_notifications() -> None: + """The public dispatcher and context refuse channels that the native modern binding does not provide.""" + listener = grpc.aio.server() + + async def notify(method: str, params: Mapping[str, Any] | None) -> None: + raise NotImplementedError + + context = GRPCDispatchContext(GRPCContext(kind="grpc", can_send_request=False, peer="test"), 1, notify) + with anyio.fail_after(5): + with pytest.raises(NoBackChannelError): + await context.send_raw_request("example/request", None) + async with grpc_server(listener).connection as dispatcher: + with pytest.raises(NoBackChannelError): + await dispatcher.send_raw_request("example/request", None) + with pytest.raises(NoBackChannelError): + await dispatcher.notify("example/event", None) + assert not context.can_send_request + + +@pytest.mark.anyio +@pytest.mark.parametrize("report_progress", [False, True]) +async def test_context_progress_is_opt_in_and_omits_absent_fields(report_progress: bool) -> None: + """Progress without an opt-in is a no-op; supplied values are forwarded without inventing total or message.""" + notifications: list[tuple[str, Mapping[str, Any] | None]] = [] + + async def notify(method: str, params: Mapping[str, Any] | None) -> None: + notifications.append((method, params)) + + context = GRPCDispatchContext( + GRPCContext(kind="grpc", can_send_request=False, peer="test"), + "request", + notify, + report_progress=report_progress, + ) + await context.progress(1) + assert notifications == ( + [("notifications/progress", {"progressToken": "request", "progress": 1})] if report_progress else [] + ) diff --git a/examples/transports/tests/test_grpc_lifecycle.py b/examples/transports/tests/test_grpc_lifecycle.py new file mode 100644 index 0000000000..e9193a2fd6 --- /dev/null +++ b/examples/transports/tests/test_grpc_lifecycle.py @@ -0,0 +1,95 @@ +"""Live cancellation and shutdown checks that require the current gRPC server to execute.""" + +from collections.abc import AsyncIterator +from contextlib import asynccontextmanager + +import anyio +import anyio.abc +import grpc.aio +import pytest +from mcp import Client, MCPError +from mcp.server.mcpserver import MCPServer +from mcp.types import CONNECTION_CLOSED, REQUEST_TIMEOUT + +from mcp_transport_examples.grpc import grpc_client, grpc_server + + +async def verify(cause: str) -> None: + entered = anyio.Event() + cancelled = anyio.Event() + stop = anyio.Event() + stopped = anyio.Event() + finished = anyio.Event() + errors: list[int] = [] + + @asynccontextmanager + async def lifespan(server: MCPServer[None]) -> AsyncIterator[None]: + try: + yield None + finally: + assert cancelled.is_set() + + server = MCPServer("gRPC cancellation", lifespan=lifespan) + + @server.tool() + async def wait() -> str: + entered.set() + try: + await anyio.sleep_forever() + finally: + cancelled.set() + raise NotImplementedError + + listener = grpc.aio.server() + port = listener.add_insecure_port("127.0.0.1:0") + + async def run_server(*, task_status: anyio.abc.TaskStatus[None]) -> None: + try: + async with server.serve() as runtime: + await runtime.connect(grpc_server(listener)) + await listener.start() + task_status.started() + await stop.wait() + finally: + with anyio.move_on_after(5, shield=True): + await listener.stop(0) + stopped.set() + + async with anyio.create_task_group() as tg: + await tg.start(run_server) + async with grpc.aio.insecure_channel(f"127.0.0.1:{port}") as channel: + async with Client(grpc_client(channel), mode="2026-07-28") as client: + + async def call(*, task_status: anyio.abc.TaskStatus[anyio.CancelScope]) -> None: + with anyio.CancelScope() as scope: + task_status.started(scope) + try: + # A real deadline is the behavior under test, not a synchronization delay. + await client.call_tool("wait", read_timeout_seconds=0.5 if cause == "timeout" else None) + except MCPError as exc: + errors.append(exc.code) + finished.set() + + async with anyio.create_task_group() as calls: + scope = await calls.start(call) + await entered.wait() + if cause == "caller": + scope.cancel() + elif cause == "runtime": + stop.set() + elif cause == "channel": + await channel.close() + await finished.wait() + await cancelled.wait() + expected = [] if cause == "caller" else [REQUEST_TIMEOUT if cause == "timeout" else CONNECTION_CLOSED] + assert errors == expected + stop.set() + await stopped.wait() + + +@pytest.mark.anyio +@pytest.mark.parametrize("cause", ["caller", "timeout", "runtime", "channel"]) +async def test_native_cancellation_finishes_the_request_and_handler(cause: str) -> None: + """Exercise this process's gRPC server, not a recorded response or an external service.""" + with anyio.fail_after(5): + await verify(cause) diff --git a/examples/transports/tests/test_grpc_response.py b/examples/transports/tests/test_grpc_response.py new file mode 100644 index 0000000000..cd063631ec --- /dev/null +++ b/examples/transports/tests/test_grpc_response.py @@ -0,0 +1,84 @@ +from collections.abc import AsyncIterator + +import anyio +import grpc +import grpc.aio +import pytest +from mcp import Client, MCPError +from mcp.types import CONNECTION_CLOSED, Request, RequestParams, Result + +from mcp_transport_examples.grpc import grpc_client +from mcp_transport_examples.rpc_pb2 import CallEvent, CallRequest, Notification + + +@pytest.mark.anyio +@pytest.mark.parametrize( + "events", + [ + [], + [CallEvent()], + [CallEvent(result_json=b"{}"), CallEvent(result_json=b"{}")], + [CallEvent(result_json=b"[]")], + [CallEvent(result_json=b'{"value": Infinity}')], + [CallEvent(notification=Notification(method="example/event", params_json=b"[]"))], + [ + CallEvent( + notification=Notification( + method="notifications/progress", params_json=b'{"progressToken":0,"progress":1}' + ) + ) + ], + [CallEvent(result_json=b'{"padding":"' + b"a" * (4 * 1024 * 1024) + b'"}')], + ], + ids=[ + "missing", + "empty-event", + "duplicate", + "array-result", + "nonfinite", + "bad-notification", + "notification-only", + "oversized", + ], +) +async def test_invalid_response_frames_fail_the_mcp_request(events: list[CallEvent]) -> None: + """A typed SDK server cannot produce these invalid frames, so a local gRPC peer sends them explicitly.""" + + async def reply( + request: CallRequest, context: grpc.aio.ServicerContext[CallRequest, CallEvent] + ) -> AsyncIterator[CallEvent]: + assert request.method == "example/response" + for event in events: + yield event + + listener = grpc.aio.server() + listener.add_generic_rpc_handlers( + [ + grpc.method_handlers_generic_handler( + "mcp.transport.example.MCP", + { + "Call": grpc.unary_stream_rpc_method_handler( + reply, + request_deserializer=CallRequest.FromString, + response_serializer=CallEvent.SerializeToString, + ) + }, + ) + ] + ) + port = listener.add_insecure_port("127.0.0.1:0") + with anyio.fail_after(5): + try: + await listener.start() + async with grpc.aio.insecure_channel( + f"127.0.0.1:{port}", options=[("grpc.max_receive_message_length", 8 * 1024 * 1024)] + ) as channel: + async with Client(grpc_client(channel), mode="2026-07-28") as client: + with pytest.raises(MCPError) as exc: + await client.session.send_request( + Request(method="example/response", params=RequestParams()), Result + ) + assert exc.value.code == CONNECTION_CLOSED + assert isinstance(exc.value.__cause__, ValueError) + finally: + await listener.stop(0) diff --git a/examples/transports/tests/test_grpc_server.py b/examples/transports/tests/test_grpc_server.py new file mode 100644 index 0000000000..4ae600297b --- /dev/null +++ b/examples/transports/tests/test_grpc_server.py @@ -0,0 +1,220 @@ +import json +from collections.abc import AsyncIterator +from contextlib import AsyncExitStack, asynccontextmanager +from typing import Any + +import anyio +import grpc +import grpc.aio +import pytest +from mcp import Client, MCPError +from mcp.client.subscriptions import ToolsListChanged +from mcp.server import Server, ServerRequestContext +from mcp.server.mcpserver import Context, MCPServer +from mcp.types import CONNECTION_CLOSED, INVALID_PARAMS, Request, RequestParams, Result +from pydantic import BaseModel + +from mcp_transport_examples.grpc import grpc_client, grpc_server +from mcp_transport_examples.rpc_pb2 import CallEvent, CallRequest + + +@asynccontextmanager +async def serving(server: Server[Any] | MCPServer[Any], *, max_requests: int = 64) -> AsyncIterator[str]: + async with AsyncExitStack() as stack: + listener = grpc.aio.server(options=[("grpc.max_receive_message_length", 8 * 1024 * 1024)]) + port = listener.add_insecure_port("127.0.0.1:0") + stack.push_async_callback(listener.stop, 0) + runtime = await stack.enter_async_context(server.serve()) + await runtime.connect(grpc_server(listener, max_requests=max_requests)) + await listener.start() + yield f"127.0.0.1:{port}" + + +@pytest.mark.anyio +@pytest.mark.parametrize( + ("params", "request_id"), + [ + (b"[]", b"0"), + (b'{"number": NaN}', b"0"), + (b"\xff", b"0"), + (b"{}", b"true"), + (b"{}", b"null"), + (b'{"padding":"' + b"a" * (4 * 1024 * 1024) + b'"}', b"0"), + ], + ids=["array", "nonfinite", "invalid-utf8", "boolean-id", "null-id", "oversized"], +) +async def test_invalid_binding_payload_is_rejected_before_mcp_dispatch(params: bytes, request_id: bytes) -> None: + """The typed MCP client cannot emit malformed protobuf-binding input, so send it over a real raw gRPC call.""" + with anyio.fail_after(5): + async with serving(Server("validation")) as target, grpc.aio.insecure_channel(target) as channel: + call = channel.unary_stream( + "/mcp.transport.example.MCP/Call", + request_serializer=CallRequest.SerializeToString, + response_deserializer=CallEvent.FromString, + ) + with pytest.raises(grpc.aio.AioRpcError) as exc: + async for _ in call( + CallRequest(method="example/invalid", params_json=params, request_id_json=request_id) + ): + raise NotImplementedError + assert exc.value.code() == grpc.StatusCode.INVALID_ARGUMENT + + +@pytest.mark.anyio +async def test_native_capacity_rejects_excess_work_and_recovers() -> None: + """A saturated binding refuses another request without preventing the admitted one from completing.""" + entered = anyio.Event() + release = anyio.Event() + server = MCPServer("capacity") + + @server.tool() + async def hold() -> str: + entered.set() + await release.wait() + return "released" + + with anyio.fail_after(5): + async with serving(server, max_requests=1) as target, grpc.aio.insecure_channel(target) as channel: + async with Client(grpc_client(channel), mode="2026-07-28") as client: + + async def first() -> None: + result = await client.call_tool("hold") + assert result.structured_content == {"result": "released"} + + async with anyio.create_task_group() as tg: + tg.start_soon(first) + try: + await entered.wait() + with pytest.raises(MCPError) as exc: + await client.list_tools() + assert exc.value.code == CONNECTION_CLOSED + cause = exc.value.__cause__ + assert isinstance(cause, grpc.aio.AioRpcError) + assert cause.code() == grpc.StatusCode.RESOURCE_EXHAUSTED + finally: + release.set() + tools = await client.list_tools() + assert [tool.name for tool in tools.tools] == ["hold"] + + +@pytest.mark.anyio +async def test_closed_runtime_refuses_calls_on_a_borrowed_listener() -> None: + """Runtime shutdown closes the MCP binding while leaving the caller's gRPC listener under its ownership.""" + listener = grpc.aio.server() + port = listener.add_insecure_port("127.0.0.1:0") + with anyio.fail_after(5): + try: + async with Server("closed runtime").serve() as runtime: + await runtime.connect(grpc_server(listener)) + await listener.start() + async with grpc.aio.insecure_channel(f"127.0.0.1:{port}") as channel: + async with Client(grpc_client(channel), mode="2026-07-28") as client: + with pytest.raises(MCPError) as exc: + await client.list_tools() + assert exc.value.code == CONNECTION_CLOSED + cause = exc.value.__cause__ + assert isinstance(cause, grpc.aio.AioRpcError) + assert cause.code() == grpc.StatusCode.UNAVAILABLE + finally: + await listener.stop(0) + + +@pytest.mark.anyio +@pytest.mark.parametrize("failure", ["mcp", "validation", "self-cancel"]) +async def test_handler_failures_settle_the_native_call(failure: str) -> None: + """Run the current server's failure paths; a replayed response would not exercise handler lifetime or conversion.""" + + class IntegerValue(BaseModel): + value: int + + async def handler(ctx: ServerRequestContext, params: RequestParams) -> Result: + assert ctx.method == "example/fail" + if failure == "mcp": + raise MCPError(code=12345, message="refused", data={"reason": "application"}) + if failure == "self-cancel": + raise anyio.get_cancelled_exc_class()() + IntegerValue.model_validate({"value": "not an integer"}) + raise NotImplementedError + + server = Server("failures") + server.add_request_handler("example/fail", RequestParams, handler) + with anyio.fail_after(5): + async with serving(server) as target, grpc.aio.insecure_channel(target) as channel: + async with Client(grpc_client(channel), mode="2026-07-28") as client: + with pytest.raises(MCPError) as exc: + await client.session.send_request(Request(method="example/fail", params=RequestParams()), Result) + assert ( + exc.value.code + == {"mcp": 12345, "validation": INVALID_PARAMS, "self-cancel": CONNECTION_CLOSED}[failure] + ) + + +@pytest.mark.anyio +async def test_null_parameters_reach_mcp_envelope_validation() -> None: + """Null is valid in the binding but lacks the MCP envelope, which the typed client normally always supplies.""" + with anyio.fail_after(5): + async with serving(Server("envelope")) as target, grpc.aio.insecure_channel(target) as channel: + call = channel.unary_stream( + "/mcp.transport.example.MCP/Call", + request_serializer=CallRequest.SerializeToString, + response_deserializer=CallEvent.FromString, + ) + events = [ + event + async for event in call(CallRequest(method="example/test", params_json=b"null", request_id_json=b"0")) + ] + assert len(events) == 1 + assert events[0].WhichOneof("payload") == "error_json" + assert json.loads(events[0].error_json)["code"] == INVALID_PARAMS + + +@pytest.mark.anyio +async def test_progress_callback_failure_does_not_abort_the_request(caplog: pytest.LogCaptureFixture) -> None: + """A client callback failure is isolated from the server result and logged with its traceback.""" + server = MCPServer("callback isolation") + + @server.tool() + async def ready(ctx: Context) -> str: + await ctx.report_progress(1, 2, "working") + return "ready" + + async def progress(progress: float, total: float | None, message: str | None) -> None: + raise RuntimeError("callback failed") + + with anyio.fail_after(5): + async with serving(server) as target, grpc.aio.insecure_channel(target) as channel: + async with Client(grpc_client(channel), mode="2026-07-28") as client: + result = await client.call_tool("ready", progress_callback=progress) + assert result.structured_content == {"result": "ready"} + records = [record for record in caplog.records if record.name == "mcp_transport_examples.grpc_response"] + assert len(records) == 1 + assert records[0].exc_info is not None + + +@pytest.mark.anyio +async def test_subscription_acknowledgment_and_events_use_the_original_rpc() -> None: + """A live listen RPC remains open while a separate tool request publishes a typed change event.""" + server = MCPServer("subscriptions") + + @server.tool() + async def announce(ctx: Context) -> str: + await ctx.notify_tools_changed() + return "announced" + + with anyio.fail_after(5): + async with serving(server) as target, grpc.aio.insecure_channel(target) as channel: + async with Client(grpc_client(channel), mode="2026-07-28") as client: + async with client.listen(tools_list_changed=True) as subscription: + result = await client.call_tool("announce") + assert result.structured_content == {"result": "announced"} + event = await anext(subscription) + assert isinstance(event, ToolsListChanged) + + +@pytest.mark.anyio +async def test_invalid_capacity_fails_before_registering_the_binding() -> None: + """Invalid configuration fails locally, without creating an RPC or binding a listening socket.""" + listener = grpc.aio.server() + with anyio.fail_after(5), pytest.raises(ValueError): + async with grpc_server(listener, max_requests=0).connection: + raise NotImplementedError diff --git a/examples/transports/tests/test_grpc_shutdown_order.py b/examples/transports/tests/test_grpc_shutdown_order.py new file mode 100644 index 0000000000..c2f934d235 --- /dev/null +++ b/examples/transports/tests/test_grpc_shutdown_order.py @@ -0,0 +1,93 @@ +"""Lifespan resources must outlive a handler performing shielded cleanup.""" + +from collections.abc import AsyncIterator +from contextlib import asynccontextmanager + +import anyio +import anyio.abc +import grpc.aio +import pytest +from mcp import Client, MCPError +from mcp.server.mcpserver import MCPServer +from mcp.types import CONNECTION_CLOSED + +from mcp_transport_examples.grpc import grpc_client, grpc_server + + +async def verify() -> None: + entered = anyio.Event() + cleanup_started = anyio.Event() + release_cleanup = anyio.Event() + cleanup_finished = anyio.Event() + lifespan_closed = anyio.Event() + stop = anyio.Event() + + @asynccontextmanager + async def lifespan(server: MCPServer[None]) -> AsyncIterator[None]: + try: + yield None + finally: + lifespan_closed.set() + assert cleanup_finished.is_set() + + server = MCPServer("shutdown order", lifespan=lifespan) + + @server.tool() + async def wait() -> str: + entered.set() + try: + await anyio.sleep_forever() + finally: + with anyio.CancelScope(shield=True): + cleanup_started.set() + await release_cleanup.wait() + assert not lifespan_closed.is_set() + cleanup_finished.set() + raise NotImplementedError + + listener = grpc.aio.server() + port = listener.add_insecure_port("127.0.0.1:0") + + async def run_server(*, task_status: anyio.abc.TaskStatus[None]) -> None: + try: + async with server.serve() as runtime: + await runtime.connect(grpc_server(listener)) + await listener.start() + task_status.started() + await stop.wait() + finally: + with anyio.move_on_after(5, shield=True): + await listener.stop(0) + + async with anyio.create_task_group() as tg: + await tg.start(run_server) + async with grpc.aio.insecure_channel(f"127.0.0.1:{port}") as channel: + async with Client(grpc_client(channel), mode="2026-07-28") as client: + + async def call() -> None: + with pytest.raises(MCPError) as exc: + await client.call_tool("wait") + assert exc.value.code == CONNECTION_CLOSED + + async with anyio.create_task_group() as calls: + calls.start_soon(call) + try: + await entered.wait() + stop.set() + await cleanup_started.wait() + # The old five-second join timeout closed lifespan while this cleanup still ran. + with anyio.move_on_after(5.1) as window: + await lifespan_closed.wait() + assert window.cancelled_caught + finally: + release_cleanup.set() + await lifespan_closed.wait() + assert cleanup_finished.is_set() + + +@pytest.mark.anyio +async def test_runtime_keeps_lifespan_alive_through_shielded_handler_cleanup() -> None: + """The live handler must finish using application state before lifespan releases it.""" + # This check intentionally holds cleanup past the old five-second deadline. + with anyio.fail_after(10): + await verify() diff --git a/examples/transports/tests/test_grpc_tls.py b/examples/transports/tests/test_grpc_tls.py new file mode 100644 index 0000000000..ef66ec69ce --- /dev/null +++ b/examples/transports/tests/test_grpc_tls.py @@ -0,0 +1,177 @@ +import ipaddress +from contextlib import AsyncExitStack +from datetime import datetime, timedelta, timezone +from typing import Literal + +import anyio +import grpc +import grpc.aio +import pytest +from cryptography import x509 +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives.asymmetric import ec +from cryptography.x509.oid import ExtendedKeyUsageOID, NameOID +from mcp import Client, MCPError +from mcp.server.context import CallNext, HandlerResult, ServerRequestContext +from mcp.server.mcpserver import Context, MCPServer +from mcp.types import ( + CLIENT_CAPABILITIES_META_KEY, + CLIENT_INFO_META_KEY, + CONNECTION_CLOSED, + PROTOCOL_VERSION_META_KEY, + CallToolRequestParams, + CallToolResult, + Implementation, +) + +from mcp_transport_examples.grpc import grpc_client, grpc_server +from mcp_transport_examples.grpc_context import GRPCContext +from mcp_transport_examples.rpc_pb2 import CallEvent, CallRequest + + +def certificate( + name: str, + key: ec.EllipticCurvePrivateKey, + issuer: x509.Name, + issuer_key: ec.EllipticCurvePrivateKey, + *, + ca: bool = False, + server: bool = False, +) -> bytes: + now = datetime.now(timezone.utc) + builder = ( + x509.CertificateBuilder() + .subject_name(x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, name)])) + .issuer_name(issuer) + .public_key(key.public_key()) + .serial_number(x509.random_serial_number()) + .not_valid_before(now - timedelta(days=1)) + .not_valid_after(now + timedelta(days=1)) + .add_extension(x509.BasicConstraints(ca=ca, path_length=None), critical=True) + ) + if not ca: + builder = builder.add_extension( + x509.ExtendedKeyUsage([ExtendedKeyUsageOID.SERVER_AUTH if server else ExtendedKeyUsageOID.CLIENT_AUTH]), + critical=False, + ) + if server: + builder = builder.add_extension( + x509.SubjectAlternativeName([x509.IPAddress(ipaddress.ip_address("127.0.0.1"))]), critical=False + ) + return builder.sign(issuer_key, hashes.SHA256()).public_bytes(serialization.Encoding.PEM) + + +def private_bytes(key: ec.EllipticCurvePrivateKey) -> bytes: + return key.private_bytes( + serialization.Encoding.PEM, serialization.PrivateFormat.PKCS8, serialization.NoEncryption() + ) + + +@pytest.mark.anyio +@pytest.mark.parametrize("security", ["insecure", "tls", "mtls", "missing-certificate", "untrusted-certificate"]) +async def test_peer_identity_comes_from_tls_not_caller_claims( + security: Literal["insecure", "tls", "mtls", "missing-certificate", "untrusted-certificate"], +) -> None: + """SDK-defined: native identity ignores caller claims; rejected TLS peers never reach middleware. + + Steps: 1. Make a typed client call. 2. Check authenticated or anonymous identity. + 3. Inject identity-looking RPC metadata, which the typed client cannot supply, and check identity again. + """ + root_key = ec.generate_private_key(ec.SECP256R1()) + root_name = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, "test root")]) + root_cert = certificate("test root", root_key, root_name, root_key, ca=True) + server_key = ec.generate_private_key(ec.SECP256R1()) + server_cert = certificate("test server", server_key, root_name, root_key, server=True) + client_key = ec.generate_private_key(ec.SECP256R1()) + issuer_key = ec.generate_private_key(ec.SECP256R1()) if security == "untrusted-certificate" else root_key + client_cert = certificate("alice", client_key, root_name, issuer_key) + client_info = Implementation(name="bob", version="1").model_dump(by_alias=True, exclude_none=True) + reached: list[str] = [] + claims: list[str | bytes | None] = [] + + async def observe(ctx: ServerRequestContext, call_next: CallNext) -> HandlerResult: + assert isinstance(ctx.transport, GRPCContext) + assert ctx.params is not None + assert ctx.params["_meta"][CLIENT_INFO_META_KEY] == client_info + reached.append(ctx.method) + claims.append(dict(ctx.transport.metadata).get("x509_common_name")) + return await call_next(ctx) + + server = MCPServer("TLS", middleware=[observe]) + + @server.tool() + async def identity(ctx: Context) -> dict[str, str | list[str] | None]: + assert ctx.request_context.method == "tools/call" + assert isinstance(ctx.transport, GRPCContext) + return { + "key": ctx.transport.peer_identity_key, + "identities": [value.decode("utf-8") for value in ctx.transport.peer_identities], + } + + with anyio.fail_after(5): + async with AsyncExitStack() as stack: + listener = grpc.aio.server() + credentials = grpc.ssl_server_credentials( + [(private_bytes(server_key), server_cert)], + root_certificates=root_cert, + require_client_auth=security != "tls", + ) + port = ( + listener.add_insecure_port("127.0.0.1:0") + if security == "insecure" + else listener.add_secure_port("127.0.0.1:0", credentials) + ) + stack.push_async_callback(listener.stop, 0) + runtime = await stack.enter_async_context(server.serve()) + await runtime.connect(grpc_server(listener)) + await listener.start() + present_certificate = security in ("mtls", "untrusted-certificate") + channel_credentials = grpc.ssl_channel_credentials( + root_certificates=root_cert, + private_key=private_bytes(client_key) if present_certificate else None, + certificate_chain=client_cert if present_certificate else None, + ) + channel = await stack.enter_async_context( + grpc.aio.insecure_channel(f"127.0.0.1:{port}") + if security == "insecure" + else grpc.aio.secure_channel(f"127.0.0.1:{port}", channel_credentials) + ) + client = await stack.enter_async_context( + Client(grpc_client(channel), mode="2026-07-28", client_info=Implementation(name="bob", version="1")) + ) + if security in ("missing-certificate", "untrusted-certificate"): + with pytest.raises(MCPError) as exc: + await client.call_tool("identity") + assert exc.value.code == CONNECTION_CLOSED + assert reached == [] + else: + result = await client.call_tool("identity") + assert result.structured_content == { + "key": "x509_common_name" if security == "mtls" else None, + "identities": ["alice"] if security == "mtls" else [], + } + assert "tools/call" in reached + assert all(claim is None for claim in claims) + rpc = channel.unary_stream( + "/mcp.transport.example.MCP/Call", + request_serializer=CallRequest.SerializeToString, + response_deserializer=CallEvent.FromString, + ) + params = CallToolRequestParams( + name="identity", + _meta={ + PROTOCOL_VERSION_META_KEY: "2026-07-28", + CLIENT_CAPABILITIES_META_KEY: {}, + CLIENT_INFO_META_KEY: client_info, + }, + ) + request = CallRequest( + method="tools/call", + params_json=params.model_dump_json(by_alias=True).encode("utf-8"), + request_id_json=b"1", + ) + events = [event async for event in rpc(request, metadata=(("x509_common_name", "mallory"),))] + assert len(events) == 1 + forged = CallToolResult.model_validate_json(events[0].result_json) + assert forged.structured_content == result.structured_content + assert claims[-1] == "mallory" diff --git a/examples/transports/tests/test_mqtt.py b/examples/transports/tests/test_mqtt.py new file mode 100644 index 0000000000..b4651e4708 --- /dev/null +++ b/examples/transports/tests/test_mqtt.py @@ -0,0 +1,28 @@ +import aiomqtt +import pytest + +from mcp_transport_examples.mqtt import mqtt_transport + + +@pytest.mark.anyio +@pytest.mark.parametrize( + ("incoming", "outgoing", "expiry", "size"), + [ + ("same", "same", 60, 4096), + ("bad/#", "response", 60, 4096), + ("request", "bad/+", 60, 4096), + ("request", "response", 0, 4096), + ("request", "response", 2**32, 4096), + ("request", "response", 60, 0), + ], +) +async def test_invalid_configuration_fails_without_connecting( + incoming: str, outgoing: str, expiry: int, size: int +) -> None: + """Adapter-defined constraints reject unsafe topic routing and limits before touching a broker.""" + client = aiomqtt.Client("unused.invalid", protocol=aiomqtt.ProtocolVersion.V5) + with pytest.raises(ValueError): + async with mqtt_transport( + client, incoming_topic=incoming, outgoing_topic=outgoing, expiry=expiry, max_message_size=size + ): + raise NotImplementedError diff --git a/pyproject.toml b/pyproject.toml index b2f26da55f..c76b44655d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -245,7 +245,7 @@ max-returns = 13 # Default is 6 max-statements = 102 # Default is 50 [tool.uv.workspace] -members = ["src/mcp-types", "examples", "examples/clients/*", "examples/servers/*", "examples/snippets"] +members = ["src/mcp-types", "examples", "examples/clients/*", "examples/servers/*", "examples/snippets", "examples/transports"] [tool.uv.sources] mcp = { workspace = true } @@ -254,6 +254,8 @@ mcp-types = { workspace = true } strict-no-cover = { git = "https://github.com/pydantic/strict-no-cover" } [tool.pytest.ini_options] +# Live broker checks belong to the optional transport example package. +testpaths = ["tests"] log_cli = true xfail_strict = true # tests/docs/ imports the docs tooling, top-level modules under scripts/docs/. diff --git a/src/mcp/client/_transport.py b/src/mcp/client/_transport.py index 0163fef950..f7ca1d30b8 100644 --- a/src/mcp/client/_transport.py +++ b/src/mcp/client/_transport.py @@ -1,21 +1,5 @@ """Transport protocol for MCP clients.""" -from __future__ import annotations - -from contextlib import AbstractAsyncContextManager -from typing import Protocol - -from mcp.shared._stream_protocols import ReadStream, WriteStream -from mcp.shared.message import SessionMessage +from mcp.shared.transport import ReadStream, Transport, TransportStreams, WriteStream __all__ = ["ReadStream", "WriteStream", "Transport", "TransportStreams"] - -TransportStreams = tuple[ReadStream[SessionMessage | Exception], WriteStream[SessionMessage]] - - -class Transport(AbstractAsyncContextManager[TransportStreams], Protocol): - """Protocol for MCP transports. - - A transport is an async context manager that yields read and write streams - for bidirectional communication with an MCP server. - """ diff --git a/src/mcp/client/client.py b/src/mcp/client/client.py index f921c7e30b..e229bf9c1f 100644 --- a/src/mcp/client/client.py +++ b/src/mcp/client/client.py @@ -71,6 +71,7 @@ from mcp.shared.extension import validate_extension_identifier from mcp.shared.jsonrpc_dispatcher import JSONRPCDispatcher from mcp.shared.subscriptions import event_to_notification +from mcp.shared.transport import DispatcherTransport logger = logging.getLogger(__name__) @@ -90,10 +91,12 @@ ``__aenter__`` reads them for the handshake step.""" -def _connect_transport(transport: Transport) -> _Connector: - """Connector for the stream-backed paths (URL, user-supplied ``Transport``).""" +def _connect_transport(transport: Transport | DispatcherTransport) -> _Connector: + """Enter a message transport or an explicitly dispatcher-backed connection.""" async def connect(exit_stack: AsyncExitStack, _mode: ConnectMode, _raise_exceptions: bool) -> Dispatcher[Any]: + if isinstance(transport, DispatcherTransport): + return await exit_stack.enter_async_context(transport.connection) read_stream, write_stream = await exit_stack.enter_async_context(transport) return JSONRPCDispatcher(read_stream, write_stream) @@ -280,12 +283,13 @@ async def main(): ``` """ - server: Server[Any] | MCPServer | Transport | StdioServerParameters | str + server: Server[Any] | MCPServer | Transport | DispatcherTransport | StdioServerParameters | str """The MCP server to connect to. If the server is a URL string, it will be used as the URL for a `streamable_http_client` transport. If the server is a `StdioServerParameters`, the command is launched with `stdio_client`. If the server is a `Transport` instance, it will be used directly. + A `DispatcherTransport` explicitly supplies a dispatcher instead of streams. If the server is a `Server` or `MCPServer` instance, it will be connected in-process. """ diff --git a/src/mcp/server/context.py b/src/mcp/server/context.py index bfcb9c9ca4..a242228498 100644 --- a/src/mcp/server/context.py +++ b/src/mcp/server/context.py @@ -47,6 +47,8 @@ class ServerRequestContext(Generic[LifespanContextT, RequestT]): request: RequestT | None = None close_sse_stream: CloseSSEStreamCallback | None = None close_standalone_sse_stream: CloseSSEStreamCallback | None = None + transport: TransportContext | None = None + """Transport metadata supplied by the dispatcher; absent on manually constructed contexts unless provided.""" # Covariant: `lifespan` is exposed read-only, so a `Context[AppState]` passes as `Context[object]`. diff --git a/src/mcp/server/lowlevel/server.py b/src/mcp/server/lowlevel/server.py index 8a886dcc24..38f78f0038 100644 --- a/src/mcp/server/lowlevel/server.py +++ b/src/mcp/server/lowlevel/server.py @@ -64,6 +64,7 @@ async def main(): from mcp.server.context import HandlerResult, ServerMiddleware, ServerRequestContext from mcp.server.models import InitializationOptions from mcp.server.runner import serve_dual_era_loop +from mcp.server.runtime import ServerRuntime from mcp.server.streamable_http import EventStore from mcp.server.streamable_http_manager import ( DEFAULT_MAX_SESSIONS, @@ -75,6 +76,7 @@ async def main(): from mcp.shared._stream_protocols import ReadStream, WriteStream from mcp.shared.exceptions import MCPDeprecationWarning from mcp.shared.message import SessionMessage +from mcp.shared.transport import TransportContextBuilder logger = logging.getLogger(__name__) @@ -689,6 +691,15 @@ def session_manager(self) -> StreamableHTTPSessionManager: ) return self._session_manager + def serve(self, *, max_connections: int = 100) -> AbstractAsyncContextManager[ServerRuntime[LifespanResultT]]: + """Share one application lifespan across custom transport connections. + + Use `await runtime.connect(transport)` inside the context for each logical + peer. Admission waits at `max_connections`; exiting cancels active + connections and closes their transports before application cleanup. + """ + return ServerRuntime[LifespanResultT].open(self, max_connections=max_connections) + async def run( self, read_stream: ReadStream[SessionMessage | Exception], @@ -699,6 +710,8 @@ async def run( # but also make tracing exceptions much easier during testing and when using # in-process servers. raise_exceptions: bool = False, + *, + transport_builder: TransportContextBuilder | None = None, ) -> None: """Serve a single connection over the given streams until the read side closes. @@ -706,7 +719,9 @@ async def run( then drives the loop, serving the legacy handshake era and the modern per-request-envelope era (the client's first request decides which). Transports with their own lifespan owner (the streamable-HTTP manager) - call `serve_loop` directly instead. + call `serve_loop` directly instead. `transport_builder` converts each + inbound message's metadata to the `transport` exposed on its handler + context. Without it, the dispatcher supplies generic JSON-RPC metadata. """ async with self.lifespan(self) as lifespan_context: await serve_dual_era_loop( @@ -716,6 +731,7 @@ async def run( lifespan_state=lifespan_context, init_options=initialization_options, raise_exceptions=raise_exceptions, + transport_builder=transport_builder, ) def streamable_http_app( diff --git a/src/mcp/server/mcpserver/context.py b/src/mcp/server/mcpserver/context.py index 07c4799dc1..6265aece59 100644 --- a/src/mcp/server/mcpserver/context.py +++ b/src/mcp/server/mcpserver/context.py @@ -24,6 +24,7 @@ ResourceUpdated, ToolsListChanged, ) +from mcp.shared.transport_context import TransportContext if TYPE_CHECKING: from mcp.server.mcpserver.server import MCPServer @@ -278,6 +279,11 @@ async def log( related_request_id=self.request_id, ) + @property + def transport(self) -> TransportContext | None: + """Transport metadata for this request, when its context supplies it.""" + return self.request_context.transport + @property def headers(self) -> Mapping[str, str] | None: """Request headers carried by this message, when the transport has them. diff --git a/src/mcp/server/mcpserver/server.py b/src/mcp/server/mcpserver/server.py index fbd2c26dd8..486336d1d5 100644 --- a/src/mcp/server/mcpserver/server.py +++ b/src/mcp/server/mcpserver/server.py @@ -90,6 +90,7 @@ from mcp.server.mcpserver.utilities.context_injection import find_context_parameter from mcp.server.mcpserver.utilities.logging import configure_logging, get_logger from mcp.server.request_state import RequestStateBoundary, RequestStateSecurity +from mcp.server.runtime import ServerRuntime from mcp.server.sse import SseServerTransport from mcp.server.stdio import stdio_server from mcp.server.streamable_http import EventStore @@ -1062,6 +1063,15 @@ def decorator( return decorator + def serve(self, *, max_connections: int = 100) -> AbstractAsyncContextManager[ServerRuntime[LifespanResultT]]: + """Share one application lifespan across custom transport connections. + + Use `await runtime.connect(transport)` inside the context for each logical + peer. Admission waits at `max_connections`; exiting cancels active + connections and closes their transports before application cleanup. + """ + return self._lowlevel_server.serve(max_connections=max_connections) + async def run_stdio_async(self) -> None: """Run the server using stdio transport.""" async with stdio_server() as (read_stream, write_stream): diff --git a/src/mcp/server/runner.py b/src/mcp/server/runner.py index 26e8efbe57..9f1657e470 100644 --- a/src/mcp/server/runner.py +++ b/src/mcp/server/runner.py @@ -67,6 +67,7 @@ from mcp.shared.inbound import InboundLadderRejection, classify_inbound_request from mcp.shared.jsonrpc_dispatcher import JSONRPCDispatcher, handler_exception_to_error_data from mcp.shared.message import MessageMetadata, ServerMessageMetadata, SessionMessage +from mcp.shared.transport import TransportContextBuilder from mcp.shared.transport_context import TransportContext if TYPE_CHECKING: @@ -81,6 +82,7 @@ "serve_connection", "serve_dual_era_loop", "serve_loop", + "serve_modern_dispatcher", "serve_one", ] @@ -334,6 +336,7 @@ def _make_context( meta=meta, protocol_version=protocol_version, request=request, + transport=dctx.transport, close_sse_stream=close_sse_stream, close_standalone_sse_stream=close_standalone_sse_stream, ) @@ -476,6 +479,7 @@ async def serve_loop( session_id: str | None = None, init_options: InitializationOptions | None = None, raise_exceptions: bool = False, + transport_builder: TransportContextBuilder | None = None, ) -> None: """Drive ``server`` in handshake-only loop mode over a stream pair until the channel closes. @@ -490,6 +494,7 @@ async def serve_loop( read_stream, write_stream, raise_handler_exceptions=raise_exceptions, + transport_builder=transport_builder, # Handle `initialize` inline so a client that pipelines it with the # next request (spec: SHOULD NOT, not MUST NOT) sees the initialized # state instead of failing the init-gate. @@ -608,6 +613,7 @@ async def serve_dual_era_loop( session_id: str | None = None, init_options: InitializationOptions | None = None, raise_exceptions: bool = False, + transport_builder: TransportContextBuilder | None = None, ) -> None: """Drive `server` over a duplex stream pair, in the era the client opens with. @@ -630,7 +636,12 @@ async def serve_dual_era_loop( ) if opens_modern: await _serve_modern_stream( - server, replayed, write_stream, lifespan_state=lifespan_state, raise_exceptions=raise_exceptions + server, + replayed, + write_stream, + lifespan_state=lifespan_state, + raise_exceptions=raise_exceptions, + transport_builder=transport_builder, ) else: await _serve_legacy_stream( @@ -641,6 +652,7 @@ async def serve_dual_era_loop( session_id=session_id, init_options=init_options, raise_exceptions=raise_exceptions, + transport_builder=transport_builder, ) finally: await write_stream.aclose() @@ -723,12 +735,14 @@ async def _serve_legacy_stream( session_id: str | None, init_options: InitializationOptions | None, raise_exceptions: bool, + transport_builder: TransportContextBuilder | None, ) -> None: """Serve a 2025 handshake connection; enveloped requests are refused.""" dispatcher: JSONRPCDispatcher[TransportContext] = JSONRPCDispatcher( read_stream, write_stream, raise_handler_exceptions=raise_exceptions, + transport_builder=transport_builder, # `initialize` inline for the same pipelining reason as `serve_loop`. inline_methods=frozenset({"initialize"}), ) @@ -759,11 +773,30 @@ async def _serve_modern_stream( *, lifespan_state: LifespanT, raise_exceptions: bool, + transport_builder: TransportContextBuilder | None, ) -> None: """Serve a 2026-07-28 connection: every request carries its own envelope.""" dispatcher: JSONRPCDispatcher[TransportContext] = JSONRPCDispatcher( - read_stream, write_stream, raise_handler_exceptions=raise_exceptions + read_stream, write_stream, raise_handler_exceptions=raise_exceptions, transport_builder=transport_builder ) + await serve_modern_dispatcher(server, dispatcher, lifespan_state=lifespan_state, raise_exceptions=raise_exceptions) + + +async def serve_modern_dispatcher( + server: Server[LifespanT], + dispatcher: Dispatcher[TransportContext], + *, + lifespan_state: LifespanT, + raise_exceptions: bool = False, + task_status: anyio.abc.TaskStatus[None] = anyio.TASK_STATUS_IGNORED, +) -> None: + """Serve per-request-envelope MCP over a wire-independent dispatcher. + + Each request is classified before entering the shared handler pipeline. + Handshake-era initialization is rejected. The dispatcher owns request + scheduling and cancellation; the caller owns application lifespan and + transport resources. Prefer `ServerRuntime.connect()` for managed serving. + """ outbound = NotifyOnlyOutbound(dispatcher) async def on_request( @@ -809,7 +842,7 @@ async def on_notify(dctx: DispatchContext[TransportContext], method: str, params finally: await aclose_shielded(connection) - await dispatcher.run(on_request, on_notify) + await dispatcher.run(on_request, on_notify, task_status=task_status) async def serve_one( diff --git a/src/mcp/server/runtime.py b/src/mcp/server/runtime.py new file mode 100644 index 0000000000..79490cdab9 --- /dev/null +++ b/src/mcp/server/runtime.py @@ -0,0 +1,158 @@ +"""Application lifespan and connection supervision for custom transports.""" + +from __future__ import annotations + +import logging +from collections.abc import AsyncIterator +from contextlib import AsyncExitStack, asynccontextmanager +from dataclasses import dataclass, field +from functools import partial +from typing import TYPE_CHECKING, Generic + +import anyio +import anyio.abc +from typing_extensions import TypeVar + +from mcp.server.runner import serve_dual_era_loop, serve_modern_dispatcher +from mcp.shared._compat import resync_tracer +from mcp.shared.transport import DispatcherTransport, Transport, TransportContextBuilder + +if TYPE_CHECKING: + from mcp.server.lowlevel.server import Server + +__all__ = ["ServerRuntime"] + +logger = logging.getLogger(__name__) +LifespanT = TypeVar("LifespanT") + + +@dataclass +class ServerRuntime(Generic[LifespanT]): + """An active server returned by `Server.serve()` or `MCPServer.serve()`. + + Each `connect()` call serves a peer with independent protocol and request-ID state. + """ + + _server: Server[LifespanT] + _lifespan_state: LifespanT + _task_group: anyio.abc.TaskGroup + _limiter: anyio.CapacityLimiter + _active: bool = field(default=True, init=False) + + @classmethod + @asynccontextmanager + async def open( + cls, server: Server[LifespanT], *, max_connections: int = 100 + ) -> AsyncIterator[ServerRuntime[LifespanT]]: + """Share one lifespan, closing connections before application cleanup. + + Cleanup has a five-second cancellation deadline per layer and must cooperate. + """ + if max_connections < 1: + raise ValueError("max_connections must be positive") + body_error: BaseException | None = None + with anyio.CancelScope() as lifespan_scope: + async with server.lifespan(server) as state: + try: + async with anyio.create_task_group() as tg: + runtime = cls(server, state, tg, anyio.CapacityLimiter(max_connections)) + try: + yield runtime + finally: + runtime._active = False + tg.cancel_scope.cancel() + except BaseException as exc: + body_error = exc + raise + finally: + lifespan_scope.shield = True + lifespan_scope.deadline = anyio.current_time() + 5 + if lifespan_scope.cancelled_caught: + logger.warning("Server lifespan cleanup exceeded five seconds") + if body_error is not None: + raise body_error + await resync_tracer() + + async def connect( + self, + transport: Transport | DispatcherTransport, + *, + session_id: str | None = None, + transport_builder: TransportContextBuilder | None = None, + ) -> None: + """Open and supervise one peer's transport until it disconnects. + + Waits for capacity, then owns the entered transport. Opening failures reach + the caller; later failures are logged and isolated. After return, caller + cancellation does not close the connection. + Dispatcher transports signal readiness through `Dispatcher.run()`; + message transports return before receiving the first MCP request. + + Args: + transport: An unopened message transport or `DispatcherTransport`. + session_id: Optional identity for a handshake-era connection. + transport_builder: Builds handler metadata for each inbound message. + + Raises: + RuntimeError: If this runtime has closed. + """ + if not self._active: + raise RuntimeError("Server runtime is closed") + if isinstance(transport, DispatcherTransport) and (session_id is not None or transport_builder is not None): + raise ValueError("Dispatcher transports supply their own context and do not use handshake-era sessions") + + async def serve(*, task_status: anyio.abc.TaskStatus[None]) -> None: + ready = False + run_error: BaseException | None = None + + class ReadyStatus: + def started(self, value: None = None) -> None: + nonlocal ready + task_status.started() + ready = True + + status = ReadyStatus() + try: + async with self._limiter: + with anyio.CancelScope() as cleanup_scope: + async with AsyncExitStack() as stack: + if isinstance(transport, DispatcherTransport): + dispatcher = await stack.enter_async_context(transport.connection) + run = partial( + serve_modern_dispatcher, + self._server, + dispatcher, + lifespan_state=self._lifespan_state, + task_status=status, + ) + else: + read, write = await stack.enter_async_context(transport) + run = partial( + serve_dual_era_loop, + self._server, + read, + write, + lifespan_state=self._lifespan_state, + session_id=session_id, + transport_builder=transport_builder, + ) + try: + if not isinstance(transport, DispatcherTransport): + status.started() + await run() + except BaseException as exc: + run_error = exc + raise + finally: + cleanup_scope.shield = True + cleanup_scope.deadline = anyio.current_time() + 5 + if cleanup_scope.cancelled_caught: + logger.warning("Transport cleanup exceeded five seconds") + if run_error is not None: + raise run_error + except Exception: + if not ready: + raise + logger.exception("Transport connection failed") + + await self._task_group.start(serve) diff --git a/src/mcp/shared/direct_dispatcher.py b/src/mcp/shared/direct_dispatcher.py index e17283afa2..cbfaa7d0e2 100644 --- a/src/mcp/shared/direct_dispatcher.py +++ b/src/mcp/shared/direct_dispatcher.py @@ -18,7 +18,8 @@ from __future__ import annotations import logging -from collections.abc import Awaitable, Callable, Mapping +from collections.abc import AsyncIterator, Awaitable, Callable, Mapping +from contextlib import asynccontextmanager from dataclasses import dataclass, field from typing import Any @@ -52,6 +53,10 @@ _Notify = Callable[[str, Mapping[str, Any] | None], Awaitable[None]] +class _DispatchClosed(Exception): + """A connection closed while an operation was running in its caller's task.""" + + @dataclass class _DirectDispatchContext: """`DispatchContext` for an inbound request on a `DirectDispatcher`. @@ -104,8 +109,11 @@ class DirectDispatcher: to have started, and once a side has closed - via `close()` or `run()` ending - `send_raw_request` raises `MCPError` (`CONNECTION_CLOSED`) and inbound requests fail the peer's call the same way instead of invoking the - handler. Notifications are fire-and-forget in both directions: after close - they are silently dropped. + handler. Closing either peer cancels active operations, including nested + back-channel calls. `run()` joins handler cleanup before returning so + application resources cannot close underneath it. Interrupted requests + fail with `CONNECTION_CLOSED`; interrupted notifications are dropped. + Notifications sent after close are also silently dropped. """ def __init__(self, transport_ctx: TransportContext, *, raise_handler_exceptions: bool = True): @@ -117,6 +125,7 @@ def __init__(self, transport_ctx: TransportContext, *, raise_handler_exceptions: self._on_notify_intercept: OnNotifyIntercept | None = None self._next_id = 0 self._in_flight_ids: set[RequestId] = set() + self._operations: dict[anyio.CancelScope, anyio.Event] = {} self._ready = anyio.Event() self._close_event = anyio.Event() self._running = False @@ -146,7 +155,11 @@ async def send_raw_request( raise MCPError(code=CONNECTION_CLOSED, message="Connection closed") if not self._running: raise RuntimeError("DirectDispatcher.send_raw_request called before run()") - return await self._peer._dispatch_request(method, params, opts) + try: + async with self._operation(self._peer): + return await self._peer._dispatch_request(method, params, opts) + except _DispatchClosed: + raise MCPError(code=CONNECTION_CLOSED, message="Connection closed") from None async def notify(self, method: str, params: Mapping[str, Any] | None, opts: CallOptions | None = None) -> None: """Send a notification by invoking the peer's `on_notify` directly. @@ -161,7 +174,11 @@ async def notify(self, method: str, params: Mapping[str, Any] | None, opts: Call if self._closed: logger.debug("dropped notification %r on closed DirectDispatcher", method) return - await self._peer._dispatch_notify(method, params) + try: + async with self._operation(self._peer): + await self._peer._dispatch_notify(method, params) + except _DispatchClosed: + logger.debug("dropped notification %r on closed DirectDispatcher", method) async def run( self, @@ -186,25 +203,40 @@ async def run( await self._close_event.wait() finally: self._running = False - self._closed = True - # run() may end via cancellation without close() ever being - # called; setting the event wakes `_wait_ready` waiters so they - # observe the closed state instead of parking forever. - self._close_event.set() + self.close() + with anyio.CancelScope(shield=True): + for finished in tuple(self._operations.values()): + await finished.wait() def close(self) -> None: + """Stop admitting work and cancel active calls; `run()` joins their cleanup.""" self._closed = True self._close_event.set() + for scope in tuple(self._operations): + scope.cancel() + + @asynccontextmanager + async def _operation(self, peer: DirectDispatcher) -> AsyncIterator[None]: + finished = anyio.Event() + with anyio.CancelScope() as scope: + self._operations[scope] = peer._operations[scope] = finished + try: + yield + finally: + self._operations.pop(scope) + peer._operations.pop(scope, None) + finished.set() + if scope.cancelled_caught: + raise _DispatchClosed def _make_context( self, on_progress: ProgressFnT | None = None, request_id: RequestId | None = None ) -> _DirectDispatchContext: assert self._peer is not None - peer = self._peer return _DirectDispatchContext( transport=self._transport_ctx, - _back_request=lambda m, p, o: peer._dispatch_request(m, p, o), - _back_notify=lambda m, p: peer._dispatch_notify(m, p), + _back_request=self.send_raw_request, + _back_notify=self.notify, request_id=request_id, _on_progress=on_progress, ) diff --git a/src/mcp/shared/dispatcher.py b/src/mcp/shared/dispatcher.py index f2ff96e7d5..e8d113e424 100644 --- a/src/mcp/shared/dispatcher.py +++ b/src/mcp/shared/dispatcher.py @@ -264,8 +264,11 @@ async def run( ) -> None: """Drive the receive loop until the underlying channel closes. - Each inbound request is dispatched to `on_request` in its own task; - the returned dict (or raised `MCPError`) is sent back as the response. + Dispatch each inbound request independently to `on_request`; the + returned dict (or raised `MCPError`) is sent back as the response. + On closure, cancel active operations and join their handler/callback + cleanup before returning. Application resources may close as soon as + this method exits; a shielded handler must not outlive that boundary. Implementations MUST offer every inbound notification to `on_notify_intercept` synchronously in receive order (via `run_notify_intercept`), handing only unconsumed ones to `on_notify`. diff --git a/src/mcp/shared/transport.py b/src/mcp/shared/transport.py new file mode 100644 index 0000000000..cac6b12994 --- /dev/null +++ b/src/mcp/shared/transport.py @@ -0,0 +1,67 @@ +"""Public contracts for message transports on either side of an MCP connection.""" + +from __future__ import annotations + +from collections.abc import Callable +from contextlib import AbstractAsyncContextManager +from dataclasses import dataclass +from typing import TypeAlias + +from typing_extensions import Protocol + +from mcp.shared._stream_protocols import ReadStream, WriteStream +from mcp.shared.dispatcher import Dispatcher +from mcp.shared.message import ClientMessageMetadata, MessageMetadata, ServerMessageMetadata, SessionMessage +from mcp.shared.transport_context import TransportContext + +__all__ = [ + "ClientMessageMetadata", + "DispatcherTransport", + "MessageMetadata", + "ReadStream", + "ServerMessageMetadata", + "SessionMessage", + "Transport", + "TransportContext", + "TransportContextBuilder", + "TransportStreams", + "WriteStream", +] + +TransportStreams: TypeAlias = tuple[ReadStream[SessionMessage | Exception], WriteStream[SessionMessage]] +TransportContextBuilder: TypeAlias = Callable[[MessageMetadata], TransportContext] + + +class Transport(AbstractAsyncContextManager[TransportStreams], Protocol): + """An async context manager yielding a logical peer's read and write streams. + + Entering opens the channel. Exiting closes owned resources and stops its + background tasks. Consumers may close the streams before context exit, so + stream closure must be idempotent. Borrowed network clients remain owned by + their caller. + + Each inbound item is a decoded `SessionMessage` or a recoverable exception. + End the read stream when the connection is lost; an exception item alone + does not fail pending requests. Writes must support cancellation and apply + backpressure instead of buffering indefinitely. + + A stream pair belongs to one logical peer, not an entire broker. The + adapter owns framing and routing; the SDK owns MCP protocol processing. + """ + + +@dataclass(frozen=True) +class DispatcherTransport: + """Explicitly opt into a dispatcher-backed connection instead of message streams. + + Pass this wrapper to `Client` or `ServerRuntime.connect()`. Entering + `connection` acquires the channel and yields an unstarted dispatcher; the + SDK owns its receive loop. Exiting releases the channel after the loop + stops. Native adapters can use their own framing without implementing MCP + negotiation, validation, callbacks, or a separate client-session API. + + Custom dispatcher implementations remain experimental until the lifecycle + contract has been validated against native network bindings. + """ + + connection: AbstractAsyncContextManager[Dispatcher[TransportContext]] diff --git a/tests/client/test_client.py b/tests/client/test_client.py index d7278e3a81..4982f8ed66 100644 --- a/tests/client/test_client.py +++ b/tests/client/test_client.py @@ -6,9 +6,11 @@ import sys from collections.abc import AsyncIterator, Iterator from contextlib import asynccontextmanager, contextmanager +from typing import Any from unittest.mock import patch import anyio +import anyio.abc import mcp_types as types import pytest from inline_snapshot import snapshot @@ -40,17 +42,40 @@ from mcp.client._memory import InMemoryTransport from mcp.client._transport import TransportStreams from mcp.client.client import Client -from mcp.client.session import ClientRequestContext +from mcp.client.session import ClientRequestContext, ClientSession from mcp.client.streamable_http import streamable_http_client from mcp.server import Server, ServerRequestContext from mcp.server.mcpserver import Context, MCPServer +from mcp.server.runtime import ServerRuntime +from mcp.shared.direct_dispatcher import create_direct_dispatcher_pair +from mcp.shared.dispatcher import Dispatcher from mcp.shared.memory import MessageStream, create_client_server_memory_streams from mcp.shared.message import SessionMessage +from mcp.shared.transport import DispatcherTransport, TransportContext from tests.interaction._connect import BASE_URL, mounted_app pytestmark = pytest.mark.anyio +@asynccontextmanager +async def dispatcher_connection(runtime: ServerRuntime[Any]) -> AsyncIterator[Dispatcher[TransportContext]]: + client_dispatcher, server_dispatcher = create_direct_dispatcher_pair() + + @asynccontextmanager + async def server_connection() -> AsyncIterator[Dispatcher[TransportContext]]: + try: + yield server_dispatcher + finally: + server_dispatcher.close() + + try: + await runtime.connect(DispatcherTransport(server_connection())) + yield client_dispatcher + finally: + client_dispatcher.close() + server_dispatcher.close() + + @pytest.fixture def simple_server() -> Server: """Create a simple MCP server for testing.""" @@ -1002,3 +1027,179 @@ async def elicitation_callback( contents=[TextResourceContents(uri="memory://gated", text="unlocked")], ) ) + + +@pytest.mark.parametrize("mode", ["auto", "2026-07-28"]) +async def test_dispatcher_transport_preserves_custom_methods_payloads_and_progress(mode: str) -> None: + """The native entry keeps arbitrary method payloads and routes progress through the usual client API.""" + + class EchoParams(types.RequestParams): + value: dict[str, Any] + + class EchoResult(types.Result): + value: dict[str, Any] + + async def echo(ctx: ServerRequestContext, params: EchoParams) -> EchoResult: + assert ctx.method == "example/echo" + assert ctx.transport is not None + assert not ctx.transport.can_send_request + await ctx.session.report_progress(1, 2, "halfway") + return EchoResult(value=params.value) + + server = Server("native") + server.add_request_handler("example/echo", EchoParams, echo) + payload = {"large": 2**63 + 1, "vendor/field": [None, {"label": "café"}]} + progress_updates: list[tuple[float, float | None, str | None]] = [] + + async def progress(progress: float, total: float | None, message: str | None) -> None: + progress_updates.append((progress, total, message)) + + with anyio.fail_after(5): + async with server.serve() as runtime: + async with Client(DispatcherTransport(dispatcher_connection(runtime)), mode=mode) as client: + result = await client.session.send_request( + types.Request(method="example/echo", params=EchoParams(value=payload)), + EchoResult, + progress_callback=progress, + ) + assert result.value == payload + assert progress_updates == snapshot([(1, 2, "halfway")]) + + +async def test_dispatcher_transport_runs_multi_round_trip_callbacks() -> None: + """A native connection uses the existing client callback/retry driver rather than a separate session API.""" + + async def handler( + ctx: ServerRequestContext, params: types.ReadResourceRequestParams + ) -> ReadResourceResult | types.InputRequiredResult: + assert params.uri == "memory://native" + if params.input_responses: + answer = params.input_responses["ask"] + assert isinstance(answer, types.ElicitResult) + assert answer.content is not None + return ReadResourceResult(contents=[TextResourceContents(uri=params.uri, text=str(answer.content["name"]))]) + return types.InputRequiredResult(input_requests={"ask": _name_elicitation()}) + + server = Server("native", on_read_resource=handler) + + async def elicitation_callback( + context: ClientRequestContext, params: types.ElicitRequestParams + ) -> types.ElicitResult: + return types.ElicitResult(action="accept", content={"name": "Alice"}) + + with anyio.fail_after(5): + async with server.serve() as runtime: + async with Client( + DispatcherTransport(dispatcher_connection(runtime)), elicitation_callback=elicitation_callback + ) as client: + result = await client.read_resource("memory://native") + assert result.model_dump(by_alias=True, mode="json") == snapshot( + { + "_meta": {"io.modelcontextprotocol/serverInfo": {"name": "native", "version": ""}}, + "ttlMs": 0, + "cacheScope": "private", + "contents": [{"uri": "memory://native", "mimeType": None, "_meta": None, "text": "Alice"}], + "resultType": "complete", + } + ) + + +async def test_dispatcher_transport_rejects_legacy_handshake_without_stopping_runtime() -> None: + """The server entry is modern-only. ClientSession exposes initialize without Client's exception-group wrapping.""" + with anyio.fail_after(5): + async with Server("native").serve() as runtime: + async with dispatcher_connection(runtime) as dispatcher, ClientSession(dispatcher=dispatcher) as session: + with pytest.raises(MCPError) as exc: + await session.initialize() + assert exc.value.code == types.UNSUPPORTED_PROTOCOL_VERSION + assert exc.value.message == snapshot( + "connection is serving the 2026-07-28 protocol; the initialize handshake is not accepted" + ) + async with Client(DispatcherTransport(dispatcher_connection(runtime))) as client: + version = client.protocol_version + assert version == "2026-07-28" + + +async def test_inprocess_client_exit_joins_handler_before_closing_lifespan() -> None: + """An in-process handler runs in its caller's task, but its resources must remain alive through cleanup.""" + entered = anyio.Event() + cleaning = anyio.Event() + release = anyio.Event() + cleaned = anyio.Event() + stop = anyio.Event() + client_closed = anyio.Event() + lifespan_closed = anyio.Event() + call_finished = anyio.Event() + + @asynccontextmanager + async def lifespan(server: MCPServer[None]) -> AsyncIterator[None]: + try: + yield None + finally: + assert cleaned.is_set() + lifespan_closed.set() + + server = MCPServer("in-process shutdown", lifespan=lifespan) + + @server.tool() + async def wait() -> str: + entered.set() + try: + await anyio.sleep_forever() + finally: + with anyio.CancelScope(shield=True): + cleaning.set() + await release.wait() + assert not lifespan_closed.is_set() + cleaned.set() + raise NotImplementedError + + async def own_client(*, task_status: anyio.abc.TaskStatus[Client]) -> None: + async with Client(server) as client: + task_status.started(client) + await stop.wait() + client_closed.set() + + async def call(client: Client) -> None: + with pytest.raises(MCPError) as exc: + await client.call_tool("wait") + assert exc.value.code == types.CONNECTION_CLOSED + call_finished.set() + + with anyio.fail_after(5): + async with anyio.create_task_group() as tg: + client = await tg.start(own_client) + tg.start_soon(call, client) + try: + await entered.wait() + stop.set() + await cleaning.wait() + await anyio.wait_all_tasks_blocked() + assert not client_closed.is_set() + assert not lifespan_closed.is_set() + finally: + release.set() + await client_closed.wait() + await call_finished.wait() + assert lifespan_closed.is_set() + + +async def test_dispatcher_transport_propagates_connection_opening_failure() -> None: + """Client construction is lazy and a native connection opening failure reaches the caller unchanged.""" + failure = OSError("connection unavailable") + opened = False + + @asynccontextmanager + async def connection() -> AsyncIterator[Dispatcher[TransportContext]]: + nonlocal opened + opened = True + raise failure + yield + + client = Client(DispatcherTransport(connection())) + assert not opened + with pytest.raises(OSError) as exc: + async with client: + raise NotImplementedError + assert exc.value is failure + assert opened diff --git a/tests/docs_src/test_client_transports.py b/tests/docs_src/test_client_transports.py index 914067c7a0..944370c5ae 100644 --- a/tests/docs_src/test_client_transports.py +++ b/tests/docs_src/test_client_transports.py @@ -4,7 +4,7 @@ import pytest -from docs_src.client_transports import tutorial001, tutorial004 +from docs_src.client_transports import tutorial001, tutorial004, tutorial005, tutorial006 from mcp import Client from mcp.client.stdio import get_default_environment from mcp.client.streamable_http import streamable_http_client @@ -19,6 +19,16 @@ async def test_the_in_memory_program_on_the_page_runs(capsys: pytest.CaptureFixt assert "Found 3 books matching 'dune'." in capsys.readouterr().out +async def test_custom_transport_example_serves_independent_peers() -> None: + """The public adapter example runs both clients through stream-backed server dispatch.""" + await tutorial005.main() + + +async def test_dispatcher_transport_example_uses_the_shared_mcp_pipeline() -> None: + """The explicit dispatcher wrapper drives a complete client/server call without message streams.""" + await tutorial006.main() + + async def test_in_memory_client_talks_to_the_server_object() -> None: """tutorial001: passing the server object connects in-process. No subprocess, no port.""" async with Client(tutorial001.mcp) as client: diff --git a/tests/server/test_runner.py b/tests/server/test_runner.py index 50e77f7134..2913abf7ec 100644 --- a/tests/server/test_runner.py +++ b/tests/server/test_runner.py @@ -13,7 +13,7 @@ from contextlib import asynccontextmanager from dataclasses import dataclass, field, replace from functools import partial -from typing import Any, cast +from typing import Any, Literal, cast import anyio import anyio.abc @@ -32,6 +32,7 @@ SERVER_INFO_META_KEY, UNSUPPORTED_PROTOCOL_VERSION, CallToolRequestParams, + CallToolResult, ClientCapabilities, EmptyResult, ErrorData, @@ -57,6 +58,7 @@ ) import mcp.server.runner +from mcp import Client from mcp.server.caching import CacheHint from mcp.server.connection import Connection, NotifyOnlyOutbound from mcp.server.context import ServerRequestContext @@ -79,8 +81,10 @@ from mcp.shared.dispatcher import CallOptions from mcp.shared.exceptions import MCPError, NoBackChannelError from mcp.shared.jsonrpc_dispatcher import JSONRPCDispatcher +from mcp.shared.memory import create_client_server_memory_streams from mcp.shared.message import MessageMetadata, SessionMessage from mcp.shared.peer import dump_params +from mcp.shared.transport import TransportStreams from mcp.shared.transport_context import TransportContext from ..shared.conftest import jsonrpc_pair @@ -2109,3 +2113,57 @@ async def test_dual_era_client_propagates_body_exception_unwrapped(server: SrvT) with pytest.raises(RuntimeError, match="boom"): async with dual_era_client(server): raise RuntimeError("boom") + + +@pytest.mark.anyio +@pytest.mark.parametrize("mode", ["legacy", "auto", "2026-07-28"]) +async def test_run_delivers_custom_transport_context_without_overriding_protocol_rules( + mode: Literal["legacy", "auto", "2026-07-28"], +) -> None: + """The SDK carries adapter metadata to handlers; modern protocol rules still deny server requests.""" + + @dataclass(kw_only=True, frozen=True) + class BrokerContext(TransportContext): + peer: str + + transport_context = BrokerContext(kind="broker", can_send_request=True, peer="alice") + + def build_context(metadata: MessageMetadata) -> BrokerContext: + return transport_context + + async def inspect_context(ctx: Ctx, params: CallToolRequestParams) -> CallToolResult: + assert params.name == "inspect" + assert isinstance(ctx.transport, BrokerContext) + return CallToolResult( + content=[], + structured_content={"peer": ctx.transport.peer, "can_send_request": ctx.transport.can_send_request}, + ) + + async def list_tools(ctx: Ctx, params: PaginatedRequestParams | None) -> ListToolsResult: + return ListToolsResult(tools=[Tool(name="inspect", input_schema={"type": "object"})]) + + app = Server("custom-transport", on_call_tool=inspect_context, on_list_tools=list_tools) + + @asynccontextmanager + async def transport() -> AsyncIterator[TransportStreams]: + async with create_client_server_memory_streams() as (client_streams, server_streams): + async with anyio.create_task_group() as tg: + tg.start_soon( + partial( + app.run, + *server_streams, + app.create_initialization_options(), + transport_builder=build_context, + ) + ) + yield client_streams + tg.cancel_scope.cancel() + + with anyio.fail_after(5): + async with Client(transport(), mode=mode) as client: + result = await client.call_tool("inspect") + assert result.structured_content == { + "peer": transport_context.peer, + "can_send_request": mode == "legacy", + } + assert transport_context.can_send_request is True diff --git a/tests/server/test_runtime.py b/tests/server/test_runtime.py new file mode 100644 index 0000000000..a94a720987 --- /dev/null +++ b/tests/server/test_runtime.py @@ -0,0 +1,634 @@ +"""Public custom-transport hosting behavior, without a network broker.""" + +from collections.abc import AsyncIterator +from contextlib import asynccontextmanager +from dataclasses import dataclass +from functools import partial +from typing import Any, Literal + +import anyio +import anyio.abc +import anyio.lowlevel +import pytest +from inline_snapshot import snapshot +from mcp_types import ( + INVALID_PARAMS, + CallToolRequestParams, + CallToolResult, + ElicitRequest, + ElicitRequestFormParams, + ElicitResult, + InputRequiredResult, + ListToolsResult, + PaginatedRequestParams, + RequestId, + TextContent, + Tool, +) + +from mcp import Client, MCPError +from mcp.server import Server +from mcp.server.context import ServerRequestContext +from mcp.server.mcpserver import Context, MCPServer +from mcp.server.request_state import RequestStateSecurity +from mcp.server.runtime import ServerRuntime +from mcp.shared.direct_dispatcher import create_direct_dispatcher_pair +from mcp.shared.dispatcher import Dispatcher, OnNotify, OnNotifyIntercept, OnRequest +from mcp.shared.memory import create_client_server_memory_streams +from mcp.shared.transport import ( + DispatcherTransport, + MessageMetadata, + TransportContext, + TransportContextBuilder, + TransportStreams, +) + +pytestmark = pytest.mark.anyio + + +@asynccontextmanager +async def connect( + host: ServerRuntime[Any], *, transport_builder: TransportContextBuilder | None = None +) -> AsyncIterator[TransportStreams]: + async with create_client_server_memory_streams() as (client_streams, server_streams): + + @asynccontextmanager + async def transport() -> AsyncIterator[TransportStreams]: + async with server_streams[0], server_streams[1]: + yield server_streams + + await host.connect(transport(), transport_builder=transport_builder) + yield client_streams + + +@pytest.mark.parametrize("highlevel", [False, True]) +@pytest.mark.parametrize("modes", [("legacy", "legacy"), ("legacy", "2026-07-28"), ("2026-07-28", "2026-07-28")]) +async def test_runtime_shares_lifespan_and_isolates_clients(highlevel: bool, modes: tuple[str, str]) -> None: + """SDK hosting runs one lifespan while peers independently negotiate and dispatch overlapping requests.""" + lifecycle: list[str] = [] + entered = {"alice": anyio.Event(), "bob": anyio.Event()} + request_ids: dict[str, RequestId | None] = {} + + @asynccontextmanager + async def lifespan(server: Server[str] | MCPServer[str]) -> AsyncIterator[str]: + lifecycle.append("startup") + try: + yield "shared-state" + finally: + lifecycle.append("shutdown") + + async def inspect(name: str, request_id: RequestId | None) -> str: + request_ids[name] = request_id + entered[name].set() + await entered["bob" if name == "alice" else "alice"].wait() + return name + + if highlevel: + app = MCPServer("peers", lifespan=lifespan) + + @app.tool() + async def echo(name: str, ctx: Context[str]) -> str: + assert ctx.request_context.lifespan_context == "shared-state" + return await inspect(name, ctx.request_context.request_id) + + else: + + async def echo_lowlevel(ctx: ServerRequestContext[str], params: CallToolRequestParams) -> CallToolResult: + assert params.name == "echo" + assert ctx.lifespan_context == "shared-state" + assert params.arguments is not None + name = params.arguments["name"] + assert isinstance(name, str) + return CallToolResult(content=[TextContent(text=await inspect(name, ctx.request_id))]) + + async def list_tools(ctx: ServerRequestContext[str], params: PaginatedRequestParams | None) -> ListToolsResult: + return ListToolsResult(tools=[Tool(name="echo", input_schema={"type": "object"})]) + + app = Server("peers", lifespan=lifespan, on_call_tool=echo_lowlevel, on_list_tools=list_tools) + + results: dict[str, str] = {} + + async def call(client: Client, name: str) -> None: + result = await client.call_tool("echo", {"name": name}) + content = result.content[0] + assert isinstance(content, TextContent) + results[name] = content.text + + with anyio.fail_after(5): + async with app.serve() as host: + async with Client(connect(host), mode=modes[0]) as alice: + async with Client(connect(host), mode=modes[1]) as bob: + async with anyio.create_task_group() as tg: + tg.start_soon(call, alice, "alice") + tg.start_soon(call, bob, "bob") + if modes[0] == modes[1]: + assert request_ids["alice"] == request_ids["bob"] + await call(alice, "alice") + assert lifecycle == ["startup"] + assert lifecycle == ["startup", "shutdown"] + assert results == {"alice": "alice", "bob": "bob"} + + +@pytest.mark.parametrize("mode", ["legacy", "auto", "2026-07-28"]) +async def test_host_exposes_adapter_metadata_in_highlevel_handlers( + mode: Literal["legacy", "auto", "2026-07-28"], +) -> None: + """Adapter context survives hosting; the modern protocol's back-channel denial remains authoritative.""" + + @dataclass(kw_only=True, frozen=True) + class BrokerContext(TransportContext): + peer: str + + metadata = BrokerContext(kind="broker", can_send_request=True, peer="alice") + + def context_builder(message_metadata: MessageMetadata) -> BrokerContext: + return metadata + + app = MCPServer("metadata") + + @app.tool() + async def inspect_context(ctx: Context) -> dict[str, str | bool]: + assert isinstance(ctx.transport, BrokerContext) + return {"peer": ctx.transport.peer, "can_send_request": ctx.transport.can_send_request} + + with anyio.fail_after(5): + async with app.serve() as host, Client(connect(host, transport_builder=context_builder), mode=mode) as client: + result = await client.call_tool("inspect_context") + assert result.structured_content == {"peer": metadata.peer, "can_send_request": mode == "legacy"} + assert metadata.can_send_request is True + + +async def test_host_releases_transport_before_application_lifespan() -> None: + """Host exit cancels a connected peer and lets its adapter clean up before the application does.""" + events: list[str] = [] + + @asynccontextmanager + async def lifespan(server: Server[None]) -> AsyncIterator[None]: + try: + yield None + finally: + await anyio.lowlevel.checkpoint() + events.append("lifespan") + + async with create_client_server_memory_streams() as (client_streams, server_streams): + + @asynccontextmanager + async def transport() -> AsyncIterator[TransportStreams]: + try: + yield server_streams + finally: + await anyio.lowlevel.checkpoint() + events.append("transport") + + with anyio.fail_after(5): + async with Server("shutdown", lifespan=lifespan).serve() as host: + await host.connect(transport()) + assert events == ["transport", "lifespan"] + with pytest.raises(anyio.EndOfStream): + await client_streams[0].receive() + + +async def test_host_survives_an_adapter_failure_after_opening(caplog: pytest.LogCaptureFixture) -> None: + """An arbitrary adapter failure after readiness is isolated to its peer and logged with a traceback.""" + crash = anyio.Event() + stopped = anyio.Event() + + async def fail() -> None: + await crash.wait() + raise OSError("broker disconnected") + + with anyio.fail_after(5): + async with Server("isolation").serve() as host: + async with create_client_server_memory_streams() as (client_streams, server_streams): + + @asynccontextmanager + async def transport() -> AsyncIterator[TransportStreams]: + try: + async with anyio.create_task_group() as tg: + tg.start_soon(fail) + yield server_streams + finally: + stopped.set() + + await host.connect(transport()) + crash.set() + await stopped.wait() + async with Client(connect(host)) as healthy: + await healthy.session.discover() + with pytest.raises(anyio.EndOfStream): + await client_streams[0].receive() + assert stopped.is_set() + errors = [record for record in caplog.records if record.name == "mcp.server.runtime"] + assert len(errors) == 1 + assert errors[0].exc_info is not None + + +async def test_host_propagates_opening_failure_without_poisoning_other_connections() -> None: + """The caller of connect receives the original opening failure and can continue using the host.""" + failure = OSError("broker unavailable") + + @asynccontextmanager + async def unavailable() -> AsyncIterator[TransportStreams]: + raise failure + yield + + with anyio.fail_after(5): + async with Server("startup").serve() as host: + with pytest.raises(OSError) as exc: + await host.connect(unavailable()) + async with Client(connect(host)) as client: + await client.session.discover() + assert exc.value is failure + + +async def test_host_admission_waits_for_a_connection_slot() -> None: + """The host opens no more than its configured number of logical peers, then admits a waiting peer on EOF.""" + opened = anyio.Event() + + with anyio.fail_after(5): + async with Server("capacity").serve(max_connections=1) as host: + async with ( + create_client_server_memory_streams() as (first_client, first_server), + create_client_server_memory_streams() as (second_client, second_server), + anyio.create_task_group() as tg, + ): + + @asynccontextmanager + async def first() -> AsyncIterator[TransportStreams]: + yield first_server + + @asynccontextmanager + async def second() -> AsyncIterator[TransportStreams]: + opened.set() + yield second_server + + await host.connect(first()) + tg.start_soon(host.connect, second()) + await anyio.wait_all_tasks_blocked() + assert not opened.is_set() + await first_client[1].aclose() + await opened.wait() + await second_client[1].aclose() + assert opened.is_set() + + +async def test_closed_host_rejects_connections_without_entering_the_transport() -> None: + """Holding a host after its context exits does not allow new peers to outlive application lifespan.""" + + @asynccontextmanager + async def transport() -> AsyncIterator[TransportStreams]: + raise NotImplementedError + yield + + with anyio.fail_after(5): + async with Server("closed").serve() as host: + pass + with pytest.raises(RuntimeError) as exc: + await host.connect(transport()) + assert str(exc.value) == snapshot("Server runtime is closed") + + +@pytest.mark.parametrize("limit", [0, -1]) +async def test_host_rejects_nonpositive_capacity_before_starting_lifespan(limit: int) -> None: + """Invalid admission limits fail before the server acquires application resources.""" + + @asynccontextmanager + async def lifespan(server: Server[None]) -> AsyncIterator[None]: + raise NotImplementedError + yield None + + with pytest.raises(ValueError) as exc: + async with Server("invalid", lifespan=lifespan).serve(max_connections=limit): + raise NotImplementedError + assert str(exc.value) == snapshot("max_connections must be positive") + + +async def test_host_shields_transport_and_lifespan_cleanup_from_parent_cancellation() -> None: + """Cancelling the host owner still lets both cleanup layers perform asynchronous resource release.""" + events: list[str] = [] + + @asynccontextmanager + async def lifespan(server: Server[None]) -> AsyncIterator[None]: + try: + yield None + finally: + await anyio.lowlevel.checkpoint() + events.append("lifespan") + + with anyio.fail_after(5): + async with create_client_server_memory_streams() as (_, server_streams): + + @asynccontextmanager + async def transport() -> AsyncIterator[TransportStreams]: + try: + yield server_streams + finally: + await anyio.lowlevel.checkpoint() + events.append("transport") + + with anyio.CancelScope() as scope: + async with Server("cancel", lifespan=lifespan).serve() as host: + await host.connect(transport()) + scope.cancel() + await anyio.sleep_forever() + assert scope.cancelled_caught + assert events == ["transport", "lifespan"] + + +@pytest.mark.parametrize("stall", ["transport", "lifespan"]) +async def test_host_abandons_unresponsive_cleanup(stall: str, caplog: pytest.LogCaptureFixture) -> None: + """SDK cleanup deadlines interrupt a stuck adapter or lifespan without parking shutdown forever.""" + interrupted = anyio.Event() + + async def cleanup(layer: str) -> None: + if layer == stall: + try: + await anyio.sleep_forever() + finally: + interrupted.set() + + @asynccontextmanager + async def lifespan(server: Server[None]) -> AsyncIterator[None]: + try: + yield None + finally: + await cleanup("lifespan") + + # The behavior under test includes the documented five-second cleanup grace. + with anyio.fail_after(10): + async with create_client_server_memory_streams() as (_, server_streams): + + @asynccontextmanager + async def transport() -> AsyncIterator[TransportStreams]: + try: + yield server_streams + finally: + await cleanup("transport") + + async with Server("stalled", lifespan=lifespan).serve() as host: + await host.connect(transport()) + assert interrupted.is_set() + records = [record for record in caplog.records if record.name == "mcp.server.runtime"] + assert len(records) == 1 + assert records[0].levelname == "WARNING" + + +async def test_runtime_bounds_transport_cleanup_after_normal_peer_eof(caplog: pytest.LogCaptureFixture) -> None: + """A peer closing its stream finishes dispatch before the adapter's bounded cleanup begins.""" + interrupted = anyio.Event() + # Normal EOF, followed by the documented five-second cleanup grace. + with anyio.fail_after(10): + async with create_client_server_memory_streams() as (client_streams, server_streams): + + @asynccontextmanager + async def connection() -> AsyncIterator[TransportStreams]: + try: + yield server_streams + finally: + try: + await anyio.sleep_forever() + finally: + interrupted.set() + + async with Server("eof").serve() as runtime: + await runtime.connect(connection()) + await client_streams[1].aclose() + await interrupted.wait() + assert interrupted.is_set() + warnings = [record for record in caplog.records if record.name == "mcp.server.runtime"] + assert len(warnings) == 1 + assert warnings[0].levelname == "WARNING" + + +async def test_runtime_preserves_body_failure_when_lifespan_cleanup_times_out() -> None: + """The cleanup deadline must not turn a failed listener into a successful context-manager exit.""" + failure = RuntimeError("listener failed") + + @asynccontextmanager + async def lifespan(server: Server[None]) -> AsyncIterator[None]: + try: + yield None + finally: + await anyio.sleep_forever() + + # This failure path includes the documented five-second cleanup grace. + with anyio.fail_after(10), pytest.RaisesGroup(RuntimeError) as exc: + async with Server("failed-listener", lifespan=lifespan).serve(): + raise failure + assert exc.value.exceptions == (failure,) + + +@pytest.mark.parametrize("option", ["session_id", "transport_builder"]) +async def test_native_runtime_rejects_stream_only_options_before_opening(option: str) -> None: + """Native dispatchers supply their contexts and do not acquire legacy sessions from stream-hosting options.""" + + @asynccontextmanager + async def connection() -> AsyncIterator[Dispatcher[TransportContext]]: + raise NotImplementedError + yield + + def builder(metadata: MessageMetadata) -> TransportContext: + raise NotImplementedError + + with anyio.fail_after(5): + async with Server("native").serve() as runtime: + with pytest.raises(ValueError) as exc: + await runtime.connect( + DispatcherTransport(connection()), + session_id="session" if option == "session_id" else None, + transport_builder=builder if option == "transport_builder" else None, + ) + assert str(exc.value) == snapshot( + "Dispatcher transports supply their own context and do not use handshake-era sessions" + ) + + +async def test_runtime_waits_for_native_dispatcher_readiness(monkeypatch: pytest.MonkeyPatch) -> None: + """A native adapter is not connected until its receive loop has installed the MCP callbacks.""" + _, dispatcher = create_direct_dispatcher_pair() + entered = anyio.Event() + release = anyio.Event() + connected = anyio.Event() + run = dispatcher.run + + async def delayed_run( + on_request: OnRequest, + on_notify: OnNotify, + on_notify_intercept: OnNotifyIntercept | None = None, + *, + task_status: anyio.abc.TaskStatus[None] = anyio.TASK_STATUS_IGNORED, + ) -> None: + entered.set() + await release.wait() + await run(on_request, on_notify, on_notify_intercept, task_status=task_status) + + monkeypatch.setattr(dispatcher, "run", delayed_run) + + @asynccontextmanager + async def connection() -> AsyncIterator[Dispatcher[TransportContext]]: + yield dispatcher + + with anyio.fail_after(5): + async with Server("readiness").serve() as runtime: + + async def connect_native() -> None: + await runtime.connect(DispatcherTransport(connection())) + connected.set() + + async with anyio.create_task_group() as tg: + tg.start_soon(connect_native) + await entered.wait() + await anyio.wait_all_tasks_blocked() + assert not connected.is_set() + release.set() + await connected.wait() + assert connected.is_set() + + +async def test_runtime_propagates_native_dispatcher_startup_failure(monkeypatch: pytest.MonkeyPatch) -> None: + """A native receive-loop startup failure reaches connect's caller rather than being logged after false readiness.""" + _, dispatcher = create_direct_dispatcher_pair() + failure = RuntimeError("could not install RPC handlers") + + async def failing_run( + on_request: OnRequest, + on_notify: OnNotify, + on_notify_intercept: OnNotifyIntercept | None = None, + *, + task_status: anyio.abc.TaskStatus[None] = anyio.TASK_STATUS_IGNORED, + ) -> None: + raise failure + + monkeypatch.setattr(dispatcher, "run", failing_run) + + @asynccontextmanager + async def connection() -> AsyncIterator[Dispatcher[TransportContext]]: + yield dispatcher + + with anyio.fail_after(5): + async with Server("startup").serve() as runtime: + with pytest.raises(RuntimeError) as exc: + await runtime.connect(DispatcherTransport(connection())) + assert exc.value is failure + + +async def test_runtime_preserves_startup_failure_when_transport_cleanup_times_out( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """A stuck adapter cleanup must not replace its receive-loop startup failure with a false-readiness error.""" + _, dispatcher = create_direct_dispatcher_pair() + failure = RuntimeError("native dispatcher failed to start") + + async def failing_run( + on_request: OnRequest, + on_notify: OnNotify, + on_notify_intercept: OnNotifyIntercept | None = None, + *, + task_status: anyio.abc.TaskStatus[None] = anyio.TASK_STATUS_IGNORED, + ) -> None: + raise failure + + monkeypatch.setattr(dispatcher, "run", failing_run) + + @asynccontextmanager + async def connection() -> AsyncIterator[Dispatcher[TransportContext]]: + try: + yield dispatcher + finally: + await anyio.sleep_forever() + + # The failure includes the documented five-second adapter cleanup grace. + with anyio.fail_after(10): + async with Server("startup").serve() as runtime: + with pytest.raises(RuntimeError) as exc: + await runtime.connect(DispatcherTransport(connection())) + assert exc.value is failure + + +async def test_runtime_keeps_request_state_bound_to_verified_peer_metadata() -> None: + """The existing principal hook binds sealed state to adapter metadata, not caller-supplied claims. + + Both peers mint state concurrently, a cross-peer replay fails, and the original peer can still complete its request. + """ + + @dataclass(kw_only=True, frozen=True) + class VerifiedPeer(TransportContext): + principal: str + + def context_builder(metadata: MessageMetadata, *, principal: str) -> VerifiedPeer: + return VerifiedPeer(kind="broker", can_send_request=True, principal=principal) + + def bind_principal(ctx: ServerRequestContext) -> str: + if not isinstance(ctx.transport, VerifiedPeer): + raise ValueError("Verified transport identity is required") + return ctx.transport.principal + + server = MCPServer( + "principals", + request_state_security=RequestStateSecurity( + keys=[b"test-key-for-principal-binding-32"], bind_principal=bind_principal + ), + ) + entered = {"alice": anyio.Event(), "bob": anyio.Event()} + + @server.tool() + async def confirm(ctx: Context) -> str | InputRequiredResult: + if ctx.input_responses is not None: + assert ctx.request_state is not None + return ctx.request_state + assert isinstance(ctx.transport, VerifiedPeer) + principal = ctx.transport.principal + entered[principal].set() + await entered["bob" if principal == "alice" else "alice"].wait() + return InputRequiredResult( + input_requests={ + "confirm": ElicitRequest( + params=ElicitRequestFormParams( + message="Confirm?", requested_schema={"type": "object", "properties": {}} + ) + ) + }, + request_state=principal, + ) + + states: dict[str, str] = {} + + async def mint(client: Client, principal: str) -> None: + result = await client.session.call_tool("confirm", allow_input_required=True) + assert isinstance(result, InputRequiredResult) + assert result.request_state is not None + states[principal] = result.request_state + + with anyio.fail_after(5): + async with server.serve() as runtime: + async with ( + Client(connect(runtime, transport_builder=partial(context_builder, principal="alice"))) as alice, + Client(connect(runtime, transport_builder=partial(context_builder, principal="bob"))) as bob, + ): + async with anyio.create_task_group() as tg: + tg.start_soon(mint, alice, "alice") + tg.start_soon(mint, bob, "bob") + with pytest.raises(MCPError) as exc: + await bob.session.call_tool( + "confirm", + input_responses={"confirm": ElicitResult(action="accept")}, + request_state=states["alice"], + meta={"principal": "alice"}, + ) + assert exc.value.code == INVALID_PARAMS + assert exc.value.message == snapshot("Invalid or expired requestState") + async with Client(connect(runtime)) as anonymous: + with pytest.raises(MCPError) as missing_identity: + await anonymous.session.call_tool( + "confirm", + input_responses={"confirm": ElicitResult(action="accept")}, + request_state=states["alice"], + ) + assert missing_identity.value.code == INVALID_PARAMS + result = await alice.call_tool( + "confirm", + input_responses={"confirm": ElicitResult(action="accept")}, + request_state=states["alice"], + meta={"principal": "bob"}, + ) + assert result.structured_content == {"result": "alice"} diff --git a/tests/shared/test_dispatcher.py b/tests/shared/test_dispatcher.py index c6ebb401ff..cf7232baa2 100644 --- a/tests/shared/test_dispatcher.py +++ b/tests/shared/test_dispatcher.py @@ -11,6 +11,7 @@ from typing import TYPE_CHECKING, Any import anyio +import anyio.abc import pytest from mcp_types import ( CONNECTION_CLOSED, @@ -573,6 +574,146 @@ def broken_intercept(method: str, params: Mapping[str, Any] | None) -> bool: assert [method for method, _ in crec.notifications] == ["notifications/survives"] +@pytest.mark.anyio +@pytest.mark.parametrize("closing_side", ["client", "server"]) +@pytest.mark.parametrize("operation", ["request", "notification"]) +async def test_direct_close_joins_in_flight_handler_cleanup(closing_side: str, operation: str) -> None: + """Closing either peer interrupts its conversation and keeps run alive until shielded handler cleanup finishes.""" + entered = anyio.Event() + cleaning = anyio.Event() + release = anyio.Event() + cleaned = anyio.Event() + finished = anyio.Event() + stopped = {"client": anyio.Event(), "server": anyio.Event()} + + async def handle() -> None: + entered.set() + try: + await anyio.sleep_forever() + finally: + with anyio.CancelScope(shield=True): + cleaning.set() + await release.wait() + cleaned.set() + + async def request( + ctx: DispatchContext[TransportContext], method: str, params: Mapping[str, Any] | None + ) -> dict[str, Any]: + assert method == "work" + await handle() + raise NotImplementedError + + async def notify(ctx: DispatchContext[TransportContext], method: str, params: Mapping[str, Any] | None) -> None: + assert method == "work" + await handle() + + client, server = create_direct_dispatcher_pair() + + async def run(dispatcher: DirectDispatcher, side: str, *, task_status: anyio.abc.TaskStatus[None]) -> None: + await dispatcher.run(request, notify, task_status=task_status) + assert cleaned.is_set() + stopped[side].set() + + async def call() -> None: + if operation == "request": + with pytest.raises(MCPError) as exc: + await client.send_raw_request("work", None) + assert exc.value.code == CONNECTION_CLOSED + else: + await client.notify("work", None) + finished.set() + + with anyio.fail_after(5): + async with anyio.create_task_group() as tg: + await tg.start(run, client, "client") + await tg.start(run, server, "server") + tg.start_soon(call) + try: + await entered.wait() + (client if closing_side == "client" else server).close() + await cleaning.wait() + await anyio.wait_all_tasks_blocked() + assert not stopped[closing_side].is_set() + release.set() + await stopped[closing_side].wait() + await finished.wait() + finally: + release.set() + client.close() + server.close() + assert cleaned.is_set() + + +@pytest.mark.anyio +@pytest.mark.parametrize("closing_side", ["client", "server"]) +async def test_direct_close_cancels_nested_backchannel_requests(closing_side: str) -> None: + """Nested calls share the conversation lifetime even though both handlers execute in the originating task.""" + entered = anyio.Event() + cleaned: list[str] = [] + finished = anyio.Event() + + async def outer( + ctx: DispatchContext[TransportContext], method: str, params: Mapping[str, Any] | None + ) -> dict[str, Any]: + assert method == "outer" + try: + return await ctx.send_raw_request("inner", None) + finally: + cleaned.append("outer") + + async def inner( + ctx: DispatchContext[TransportContext], method: str, params: Mapping[str, Any] | None + ) -> dict[str, Any]: + assert method == "inner" + entered.set() + try: + await anyio.sleep_forever() + finally: + cleaned.append("inner") + raise NotImplementedError + + async def unused_notify( + ctx: DispatchContext[TransportContext], method: str, params: Mapping[str, Any] | None + ) -> None: + raise NotImplementedError + + client, server = create_direct_dispatcher_pair() + + async def call() -> None: + with pytest.raises(MCPError) as exc: + await client.send_raw_request("outer", None) + assert exc.value.code == CONNECTION_CLOSED + finished.set() + + with anyio.fail_after(5): + async with anyio.create_task_group() as tg: + await tg.start(client.run, inner, unused_notify) + await tg.start(server.run, outer, unused_notify) + tg.start_soon(call) + await entered.wait() + (client if closing_side == "client" else server).close() + await finished.wait() + client.close() + server.close() + assert cleaned == ["inner", "outer"] + + +@pytest.mark.anyio +async def test_direct_notification_handler_errors_are_not_mistaken_for_connection_shutdown() -> None: + """Shutdown drops interrupted notifications without swallowing an MCPError raised by a live handler.""" + failure = MCPError(code=CONNECTION_CLOSED, message="handler refusal") + + async def notify(ctx: DispatchContext[TransportContext], method: str, params: Mapping[str, Any] | None) -> None: + assert method == "example/event" + raise failure + + with anyio.fail_after(5): + async with running_pair(direct_pair, server_on_notify=notify) as (client, *_): + with pytest.raises(MCPError) as exc: + await client.notify("example/event", None) + assert exc.value is failure + + if TYPE_CHECKING: _d: Dispatcher[TransportContext] = DirectDispatcher(TransportContext(kind="direct", can_send_request=True)) _o: Outbound = _d diff --git a/uv.lock b/uv.lock index 40b563e974..b6cad20455 100644 --- a/uv.lock +++ b/uv.lock @@ -3,7 +3,8 @@ revision = 3 requires-python = ">=3.10" resolution-markers = [ "python_full_version >= '3.14'", - "python_full_version < '3.14'", + "python_full_version >= '3.11' and python_full_version < '3.14'", + "python_full_version < '3.11'", ] [manifest] @@ -24,6 +25,7 @@ members = [ "mcp-sse-polling-client", "mcp-sse-polling-demo", "mcp-structured-output-lowlevel", + "mcp-transport-examples", "mcp-types", ] build-constraints = [ @@ -40,6 +42,86 @@ build-constraints = [ { name = "uv-dynamic-versioning", specifier = "==0.14.0" }, ] +[[package]] +name = "aio-pika" +version = "9.6.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +dependencies = [ + { name = "aiormq", version = "6.9.4", source = { registry = "https://pypi.org/simple" } }, + { name = "exceptiongroup" }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/63/56354526f2e6e915c93bee6e4dedb35888fe82d6bc1a19f35f5a77e795ff/aio_pika-9.6.2.tar.gz", hash = "sha256:c49e9246080dc8ffa1bb0e4aca407bf3d8ad78c3ee3a93df88b68fe65d7a49b9", size = 70851, upload-time = "2026-03-22T19:03:20.878Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/05/256fa313f48bed075056d13593b92ce804be05d75f4f312be24edb82860a/aio_pika-9.6.2-py3-none-any.whl", hash = "sha256:2a5478af920d169795071c9c09c7542cd8cdece60438cf7804533dcbcce93b7f", size = 56269, upload-time = "2026-03-22T19:03:19.558Z" }, +] + +[[package]] +name = "aio-pika" +version = "10.0.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.11' and python_full_version < '3.14'", +] +dependencies = [ + { name = "aiormq", version = "7.0.0", source = { registry = "https://pypi.org/simple" } }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8f/7f/01f4ea7fe3490194420bb52e596b9619092ed13c5a230014b02075c3bd77/aio_pika-10.0.1.tar.gz", hash = "sha256:96ec3ef748ca7a25a9d2fa6e511c16c3ffcfa6b1f40ade79b8a5baabba682efd", size = 70882, upload-time = "2026-07-09T13:31:35.709Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/65/3f/329d0e52f994349ff7449c714c242ad65f14586b0e205ca632ac817fda72/aio_pika-10.0.1-py3-none-any.whl", hash = "sha256:12120a3cf8022d2a8bc5dc89e716512a38bf742c24c5562f54764af27eec7edd", size = 56332, upload-time = "2026-07-09T13:31:33.634Z" }, +] + +[[package]] +name = "aiomqtt" +version = "2.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "paho-mqtt" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/70/44/cfc58272783a11729462dc6df5adbfeabd084f840f609054ac772ae98c19/aiomqtt-2.5.1.tar.gz", hash = "sha256:25a0a47d157e8f158d2da1110ea4786c0615518751e94f7b04976c977a8ff20d", size = 86641, upload-time = "2026-03-05T18:28:56.421Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/9e/5089fa596220bf0dc73deeb23db27904e4b3504986caf08571f6f5cb84a8/aiomqtt-2.5.1-py3-none-any.whl", hash = "sha256:fd58c3593160e4d475d90ce911cdfc4239cd64de96b0ba22edf6c86bd7afa278", size = 16051, upload-time = "2026-03-05T18:28:55.14Z" }, +] + +[[package]] +name = "aiormq" +version = "6.9.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +dependencies = [ + { name = "pamqp", version = "3.3.0", source = { registry = "https://pypi.org/simple" } }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6c/0e/db90154d52d399108903fe603e5110a533c42065180265dd003788264080/aiormq-6.9.4.tar.gz", hash = "sha256:0e7c01b662804e1cc7ace9a17794e8c1192a27fc2afa96162362a6e61ae8e8ef", size = 49232, upload-time = "2026-03-23T09:18:19.493Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6c/48/1ce3773f392f02ceda37aee168fade9d725483a9592c202d06044cd093ff/aiormq-6.9.4-py3-none-any.whl", hash = "sha256:726a8586695e863fba68cf88842065ab12348c9438dcebdfc9d0bddaf6083277", size = 32166, upload-time = "2026-03-23T09:18:17.523Z" }, +] + +[[package]] +name = "aiormq" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.11' and python_full_version < '3.14'", +] +dependencies = [ + { name = "pamqp", version = "4.0.1", source = { registry = "https://pypi.org/simple" } }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/00/16/7e1c2bb887db6cbad191db9a1562e1cf5c0c61ad93f194ddc7baf5661f02/aiormq-7.0.0.tar.gz", hash = "sha256:f524121f1afbb875f50235b2748f81331e3be47542ee600e83c321c4e97ea168", size = 49231, upload-time = "2026-07-09T11:40:51.775Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/53/88/8da3627882f6bd75f780f87e46d0b58da99332c1b71d038db7a127a80648/aiormq-7.0.0-py3-none-any.whl", hash = "sha256:df49bb2282e5374a28507c4c43948e8c8e5321590f2998781c2d90a34e100789", size = 32152, upload-time = "2026-07-09T11:40:50.508Z" }, +] + [[package]] name = "annotated-types" version = "0.7.0" @@ -166,6 +248,61 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/63/c5/092e631bc1fba86f0a822be65c137c90a71b71ba0a0865e7e9a21f6ca05e/blockbuster-1.5.27-py3-none-any.whl", hash = "sha256:f0acf153d22a791bf5f142935332ef8530960ec215541b48a6037e6cea0a8645", size = 13517, upload-time = "2026-08-17T23:53:14.625Z" }, ] +[[package]] +name = "cassetter" +version = "0.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/51/8f/e934ce0b21f7045181b786412b5326d6f89b4f7f58303d4460054bc93872/cassetter-0.11.0.tar.gz", hash = "sha256:7578203459c08623f8f27e6e9179774359ceb101b9f207308274fdaae9e4d589", size = 102607, upload-time = "2026-09-10T12:06:54.626Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/5f/cb2d42df62d26b10c8f9e0ec227c08e992eff64f3a16b709f4823fda8fce/cassetter-0.11.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:af6aab0015cce3eadeebec7fbb15612c457aa482664170f6c7f7966fdd03b8ea", size = 1980907, upload-time = "2026-09-10T12:05:48.859Z" }, + { url = "https://files.pythonhosted.org/packages/31/45/25563caa90b94766a0bcd1473444aeccdddf6ee682e789fd3a24286eb5b2/cassetter-0.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ae0ba29a86fd290c2b0e19f39c598ec9176544d005de53a40a0c7a2eec332a49", size = 1857688, upload-time = "2026-09-10T12:05:50.531Z" }, + { url = "https://files.pythonhosted.org/packages/45/23/03e1902699ba6c9d6c849a50dacfe6e6a6937ce08589c0ee95b785a58b65/cassetter-0.11.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ff5729b084a201c0ff789272846b4876f4bd008b47d0929ce233727ac13fbdf", size = 1926524, upload-time = "2026-09-10T12:05:52.058Z" }, + { url = "https://files.pythonhosted.org/packages/f7/fe/9bc4776fb99ed57878e790c15c76e46532041cecc8d3587ae1537647a0cb/cassetter-0.11.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d5b9bfdeae7179ae79560180c5938754722d74b12b1c6f5a5845166da0ed2c5", size = 2070007, upload-time = "2026-09-10T12:05:53.701Z" }, + { url = "https://files.pythonhosted.org/packages/34/f0/405f4eabd04c122074618883a6810bbbe30f3e45dbc8da367b38390aa906/cassetter-0.11.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7c20f1a62a9394a9ddf2b09966a45698aa7d231a5016e6409cdace6d8157a019", size = 2112086, upload-time = "2026-09-10T12:05:55.421Z" }, + { url = "https://files.pythonhosted.org/packages/98/fc/bbfda42e4756945d9a4796bb37b7852c90248dbc73a7812f34f64239c00a/cassetter-0.11.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8feab7f50cd9ef59860f3d68883a8cc729ca780589d3e1ab6bee5da20e316c9a", size = 2297899, upload-time = "2026-09-10T12:05:57.111Z" }, + { url = "https://files.pythonhosted.org/packages/29/e7/74aa131e5d63d9b96e2e5d83bad908257e0d808668132bb41428f3b36784/cassetter-0.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:136964f2486aa8dad45e39517c4b43ad7faee30cf104f16736669d72ece73c4d", size = 2019629, upload-time = "2026-09-10T12:05:58.496Z" }, + { url = "https://files.pythonhosted.org/packages/d8/e3/e2be9ba63fa2720541e931332b31c7af2874caee2d368b4d8ed277ade158/cassetter-0.11.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:421178c75b31101c8c0af6b9b36a3ecdb55858dadc7767e498e9f7791a6d9f95", size = 1980953, upload-time = "2026-09-10T12:05:59.97Z" }, + { url = "https://files.pythonhosted.org/packages/36/de/ebe6dfc0d32c9b033f154a7a336d66eb93f4456d7335340e621b75129244/cassetter-0.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:52db9ee46971ffb68c3cf0df384dec15f6026df4352305ebe0bdf603d0532ee3", size = 1857850, upload-time = "2026-09-10T12:06:01.383Z" }, + { url = "https://files.pythonhosted.org/packages/79/49/b9f8f250c3d5817f165659561a5a007f6e3cf8e3984f3275ab6c8ca69c21/cassetter-0.11.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37de99fc2e71dd0b6231fb6087b363bf8c244ba5a095a0166768e14c233c7511", size = 1926553, upload-time = "2026-09-10T12:06:02.699Z" }, + { url = "https://files.pythonhosted.org/packages/87/c3/88f91278457a07a8186d65cf9aae9166ec1eb8a939ca18e23a9d9294b06c/cassetter-0.11.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5a2a77aaf2adb80a0f5c7860536d423d0e10fb579a3703f64e27eaf71111e75", size = 2070025, upload-time = "2026-09-10T12:06:04.069Z" }, + { url = "https://files.pythonhosted.org/packages/4d/7f/fc850ebdaad4467f103ae9d5939295be812447e0c6a72de15378e3663615/cassetter-0.11.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e39eab159142285b7b743a5ad64046bc4e3c38dba0383797737e45ecc3323b4f", size = 2111806, upload-time = "2026-09-10T12:06:05.7Z" }, + { url = "https://files.pythonhosted.org/packages/17/dd/94f3b7f6119878a45e8d193f64aead700776dd98183880f0e8067db38671/cassetter-0.11.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b03bed6a5842a26347d32bcfb0c1f4a87cc81ef2324e8d7677584036fa676706", size = 2297752, upload-time = "2026-09-10T12:06:07.519Z" }, + { url = "https://files.pythonhosted.org/packages/35/3b/6f7688fc36c40b20d6c2c6e81373117fa79cd4e705bae5ea98d488919180/cassetter-0.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:498eb6718c401746e432e365ba322920351bbb2f82260956b30b29e3b5a82ba4", size = 2019574, upload-time = "2026-09-10T12:06:09.599Z" }, + { url = "https://files.pythonhosted.org/packages/12/96/efe27ef22c195539e76c079391a39aff0b9791b98881cefc8c2680ff89e9/cassetter-0.11.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0e37ba7df95d7132a109d126889955541176902a830ac386a6061b716efc74f1", size = 1992016, upload-time = "2026-09-10T12:06:11.186Z" }, + { url = "https://files.pythonhosted.org/packages/46/82/7ca66ce3d58069692ce948437940fc657954c0047d63c57e8d1ebe21bc96/cassetter-0.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e035f81fc4096ba809b33d7dadf56233d8b77b6738f7458e52937fbc796d92aa", size = 1852366, upload-time = "2026-09-10T12:06:12.696Z" }, + { url = "https://files.pythonhosted.org/packages/b5/cc/8375f65d6f1385194570097c45018a01655023e35fbe457df09cfb61f492/cassetter-0.11.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c905cbd42dd322ed67bf2127c8b7b23ace222232f935427b79b6b902f470aff", size = 1924201, upload-time = "2026-09-10T12:06:14.283Z" }, + { url = "https://files.pythonhosted.org/packages/db/6a/d4094c2cd57d2f180e61cab004c6f78d3563f70002722e2aa0fbfc566215/cassetter-0.11.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a4c36109d40ceadb60024a011413efc9ab9f71ffb42fcd2c37bb8bb4dcf82d13", size = 2069606, upload-time = "2026-09-10T12:06:15.681Z" }, + { url = "https://files.pythonhosted.org/packages/df/04/c5b3e374d92427ed5a124a418a5e31e7bdb52932d9ca65fbe89d90bedc05/cassetter-0.11.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7b3086a2c180d854562c3ae2d455d114751c90783db81ccf83f63d1bed699722", size = 2110256, upload-time = "2026-09-10T12:06:16.996Z" }, + { url = "https://files.pythonhosted.org/packages/b7/33/7daa996190ac39686021d6df7d5c734e6762d8e444c77e29b5702fff4353/cassetter-0.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9c605d865c07b2bcb328cbf1d380d543a1703f1384a279fdae853e092d437cab", size = 2297399, upload-time = "2026-09-10T12:06:18.398Z" }, + { url = "https://files.pythonhosted.org/packages/d9/33/c0245b72c375c7e08ea4528ad53078b7b21053b1f665d7cf20252f131930/cassetter-0.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:c2f2967c91d87af8798c2f4ec88dd2be7b190b34d3ba20732bf612ebc35faf93", size = 2016727, upload-time = "2026-09-10T12:06:19.691Z" }, + { url = "https://files.pythonhosted.org/packages/a0/60/571036d38c0c98109200459ba2deb7bfe3db2354f481c8b9f559d5a99c71/cassetter-0.11.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:074242e9febdfdc8733d8b1ee3635d80af17c0ce68e501249b09c113ce6cdeee", size = 1992011, upload-time = "2026-09-10T12:06:21.247Z" }, + { url = "https://files.pythonhosted.org/packages/ae/ce/3f7d5d52e183eab67da5dad1403bd7ca3e27b4df179dec07439a6e1479b3/cassetter-0.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7a65cd317af3ebe7eb329e178bccabc56ce06cc6c40e023fc34bcbf5a5ff317e", size = 1852584, upload-time = "2026-09-10T12:06:22.691Z" }, + { url = "https://files.pythonhosted.org/packages/86/9a/363567977feb6797d394bd5267057f7db5e67e6d9a615150f104b70cd9a6/cassetter-0.11.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0225a7264649a1db81d81e86f779e3d0fcc189006109f446bdfd6bb31f890464", size = 1924781, upload-time = "2026-09-10T12:06:24.08Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ae/8b910cb80abe925494f4dcb40cd61bf66ba1fb96f98d45bbd21860cc64db/cassetter-0.11.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0e40aff3a75ed10d309dc4e05d750bfa1873c4a6d8efd57eb1027b1fd415072", size = 2069427, upload-time = "2026-09-10T12:06:26.114Z" }, + { url = "https://files.pythonhosted.org/packages/76/fa/9dad53bf7b94add575e936d985998c18e4445d6d826426841e1084b2df13/cassetter-0.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:70803d4a3f3f0b631c648fc0c07cde2947b91f9694345a7a5172d0a60567d51c", size = 2109980, upload-time = "2026-09-10T12:06:27.805Z" }, + { url = "https://files.pythonhosted.org/packages/8f/ab/b15e1a3ce73a4cbef98a101940c344a681716a9cdf348ed2bdf0b8f0ede6/cassetter-0.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3f6a1ccb97b70a3db83ffddf1fab5d6e2978f2837d800acc08c2079a57dfef2b", size = 2297450, upload-time = "2026-09-10T12:06:29.524Z" }, + { url = "https://files.pythonhosted.org/packages/bd/cd/0c3b62801c1787c345e23168f8b08383b09fe612a5df3559a14522479ac3/cassetter-0.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:6bef0417a2babf8dd5b39a3187466bf647591d2bbef6a0db57d5717edd23d911", size = 2016955, upload-time = "2026-09-10T12:06:31.146Z" }, + { url = "https://files.pythonhosted.org/packages/38/11/cc8e13ff2446653a609faed97aca79329c0164033286167dcb1eee740f3c/cassetter-0.11.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:ace65b75f1f79a57ebf86037521a8c6050393a1cf933222405f6effe7ca6ff39", size = 1993017, upload-time = "2026-09-10T12:06:32.543Z" }, + { url = "https://files.pythonhosted.org/packages/35/25/79d95436827031edb0cd5fec82536f0765079cd4f20bb55f7021ea1eb293/cassetter-0.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:86196ca7af873231a8339616e11ae0b6159657e22c7b148c152d0998c2461a7b", size = 1854089, upload-time = "2026-09-10T12:06:34.197Z" }, + { url = "https://files.pythonhosted.org/packages/ff/40/0dac2e31cde85601fb6c757a4398fddb04dc816f5f807bcd829ea0f62ef0/cassetter-0.11.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:002b8bb2a9cb3250a9ad9c751278d32500a74548fefa5759f475eee86451d3d3", size = 1925672, upload-time = "2026-09-10T12:06:35.745Z" }, + { url = "https://files.pythonhosted.org/packages/d6/7f/e78e26fd90df1caef9070ffb74ad92e34390aa5e2a08160b836c22fefdbe/cassetter-0.11.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f70b6a46a8d89731a40baa6ca5042d944aeb201d89a56318f2c647b24de09bad", size = 2070668, upload-time = "2026-09-10T12:06:37.46Z" }, + { url = "https://files.pythonhosted.org/packages/24/01/763366bc70c0394b3b677648e0a217b7b017bc52ba51365eef0e8005d32e/cassetter-0.11.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:13214fc29329b21202259848cc85522ff1482e582fddd76683bac884afdf8545", size = 2110732, upload-time = "2026-09-10T12:06:38.903Z" }, + { url = "https://files.pythonhosted.org/packages/37/5f/ad7745cea2df68936942da76df3c176a0a581378c6171db91c2ed6a88271/cassetter-0.11.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7808afd0a4312b6b23b5befd2a1b50b22e5575bfda0340d13249e38200ef9a35", size = 2298322, upload-time = "2026-09-10T12:06:40.866Z" }, + { url = "https://files.pythonhosted.org/packages/89/57/383494395c4a4bdc27d9bcfb03e0c7fabd9cf2e8fdbc573d280c0352db38/cassetter-0.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:071c31f1a96de84cd8c6f699c6da19a2b38cf74d27bfa61d051327db1e994e8e", size = 2017296, upload-time = "2026-09-10T12:06:42.284Z" }, + { url = "https://files.pythonhosted.org/packages/b0/33/d100a48e7fcd065b80aca95eadf28531dcd6c686f818cf99f3e402a5ed85/cassetter-0.11.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:3872fba7fde12ccc860aff7d13cc3e6aefdc4dbe65bf6c9de8b73dcf8330e0a5", size = 1988047, upload-time = "2026-09-10T12:06:43.743Z" }, + { url = "https://files.pythonhosted.org/packages/b6/de/c2c175b5c09a1143cbd472d3270a464836d7c9b0b209f64cac8f2402f258/cassetter-0.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6c602d42f992d95c9eea42171bc7844aca0531c94e385ae70afc218d7abc8ae1", size = 1848022, upload-time = "2026-09-10T12:06:45.227Z" }, + { url = "https://files.pythonhosted.org/packages/e1/86/34936390872c5f5f24f09a2d96df878942807905e3b6a2551da136572d8f/cassetter-0.11.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:467562e1d0b8a19ee0eebb294397ea0973a064d1143bbc987d716ec83ffc2a69", size = 1918666, upload-time = "2026-09-10T12:06:46.77Z" }, + { url = "https://files.pythonhosted.org/packages/54/bb/cdc849e32bf111e7e5613554f1b6b5b3eb9e8133043d3ec3ac5c1260da0a/cassetter-0.11.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:57020ab03f7cda137310081628af2336347c0be3f64709455570f5bfebc4f411", size = 2065273, upload-time = "2026-09-10T12:06:48.47Z" }, + { url = "https://files.pythonhosted.org/packages/79/66/3740f408774d3c9f7d950d62a968530b468aba7320945ebdc931e94f97e1/cassetter-0.11.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:aedd8c76cc4ca3302c8ed4953b82737b64479e5d15c80d186e5051f5aaa3fea9", size = 2104326, upload-time = "2026-09-10T12:06:50.001Z" }, + { url = "https://files.pythonhosted.org/packages/59/a4/9b22abd9365e1e6f1b0208882b19a606347158e7c7e61fd21e25561e5a0a/cassetter-0.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:72fd743caadef19b0ddeac453fa1acff62c746998e81656706654a5f51cb1048", size = 2294473, upload-time = "2026-09-10T12:06:51.516Z" }, + { url = "https://files.pythonhosted.org/packages/47/10/634e5d897e03beaf808978c337d2b80dc02905570afce0cfec0d71b99910/cassetter-0.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:18997a61a6da1598d348506502849d5a3129727715d8eed599b8b451789b432b", size = 2016101, upload-time = "2026-09-10T12:06:53.313Z" }, +] + +[package.optional-dependencies] +grpc = [ + { name = "grpcio" }, +] + [[package]] name = "certifi" version = "2025.8.3" @@ -637,6 +774,140 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e4/d3/5268aeabf2ad82658c4e2ff3a060648d0f02f3926cb53247c0e4d0dab49e/griffelib-2.1.0-py3-none-any.whl", hash = "sha256:cc7b3d2d2865ad0b909fcc38086e3f554b5ea7acbaa7bbb7ecaa3f5dfb7d9f00", size = 142560, upload-time = "2026-06-19T12:05:38.742Z" }, ] +[[package]] +name = "grpcio" +version = "1.84.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3f/4f/4435c0aae54657258d9cfcba78598f3d9e5fe4c82ff18d78558567b90faf/grpcio-1.84.0.tar.gz", hash = "sha256:19aaf172fc2edbefccce3f6e92c5150975dbe56c45744e9e87cf72ebdf85bfbe", size = 13493876, upload-time = "2026-09-14T06:59:33.291Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/4b/a0dc421d049b743093eae90caeb5dd92ced7226cd4919dc4de34c81455b6/grpcio-1.84.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:71fd60e6e426d293d0a2f685115ad0a0845117602cf13605a4be7524fb5f7bba", size = 6450049, upload-time = "2026-09-14T06:56:48.72Z" }, + { url = "https://files.pythonhosted.org/packages/f7/41/90292bf55af7aa09de0e3ec928d1b8c56d477f85244f7928d2231630b781/grpcio-1.84.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:8e1a45d174b6b8589f51dce1cea804aa6c1f72c9c80cba91ae2caabeb6d90540", size = 12344932, upload-time = "2026-09-14T06:56:51.685Z" }, + { url = "https://files.pythonhosted.org/packages/e9/68/b6c0248266a378b1bde08e4de7d69f3cc08ee6f5937a5dce7d3c3ba0fe1d/grpcio-1.84.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:efb29f8633bf6630dc89de4fe0353ac3d7e4b70ef7b6e29fb40f00e68c127fa5", size = 7030162, upload-time = "2026-09-14T06:56:54.412Z" }, + { url = "https://files.pythonhosted.org/packages/70/2b/0a2a2cbcf48847f83eb51fb982116d0965f2fe068e73f28b2d17facf30a4/grpcio-1.84.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:d0fdd25faece8a1f95e8a3a8006e29701b5cf8dadb4a8132e68f3134637004a5", size = 7781546, upload-time = "2026-09-14T06:56:56.571Z" }, + { url = "https://files.pythonhosted.org/packages/a6/7c/da97476f3c2e90e9f00bfb19def7cbb5f841b7661e3cd09c6a89beaa5b98/grpcio-1.84.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:393d8a78bff6731ecc5ad2151a821f8fbc1709b137ebb9c25a4ef399fbdcc914", size = 7186279, upload-time = "2026-09-14T06:56:58.817Z" }, + { url = "https://files.pythonhosted.org/packages/14/16/27fa3aed1ee6fdcbb978a1bd4bce255dc0122b90179535177a96543d14fc/grpcio-1.84.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:fc66cb50c93554b86db0b6625ab5c6e9051dbf8847c08d93c84918e02e413fb7", size = 7731191, upload-time = "2026-09-14T06:57:02.447Z" }, + { url = "https://files.pythonhosted.org/packages/4c/78/75644af37af85afb381376aef99cad92da8bc2d56ba3e5ae070a7cb59682/grpcio-1.84.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:455ed6083353b8e938f1d58c765eab2fbb165731e5b507be30fee344915a2a11", size = 8790443, upload-time = "2026-09-14T06:57:04.623Z" }, + { url = "https://files.pythonhosted.org/packages/95/4d/ce57fa986e93c06ef867f64e1ebe419e924fdc2395115607f0725f4855e4/grpcio-1.84.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d6a82c4fc6c85f2fb7572c86bdb86f84c97b6580e5f6599f711800bac48a5d8", size = 8138068, upload-time = "2026-09-14T06:57:07.348Z" }, + { url = "https://files.pythonhosted.org/packages/ba/a6/22a73111c4f75da9450bf0481fac805396ec9bf6f949a90bb2969de07cf4/grpcio-1.84.0-cp310-cp310-win32.whl", hash = "sha256:8e3f508d0e9e6236ba2f08d56e33355e434e785e813149a1b8477d3edf69779d", size = 4496545, upload-time = "2026-09-14T06:57:09.236Z" }, + { url = "https://files.pythonhosted.org/packages/31/ff/dc048bc3d8ebd8d4b7f6f6803c76142a9a5ca1e1e9fa34e79597f0f9ed77/grpcio-1.84.0-cp310-cp310-win_amd64.whl", hash = "sha256:ed2c1493c44d0932f1e55fdb5d1ead658c68288ec5d51b8c4928422d98633ef9", size = 5258144, upload-time = "2026-09-14T06:57:11.403Z" }, + { url = "https://files.pythonhosted.org/packages/2d/b9/46146728b3f4a5c7e34c17d0ab724d58b5456b116e76dc77d3ef4e79b135/grpcio-1.84.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:4aaeceeb7fa7d824c322d1ec3208c8495c88478a927295553235435fc49043ad", size = 6454572, upload-time = "2026-09-14T06:57:14.651Z" }, + { url = "https://files.pythonhosted.org/packages/e3/63/5d668b4102637410d700153fd12d6a798e3ff8308bd9dcbaeae93f191060/grpcio-1.84.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:06619ba1515e5ee69fb2a514e95dd8be05ce74cb3928d5b34f87f87c86fe3c27", size = 12359529, upload-time = "2026-09-14T06:57:17.202Z" }, + { url = "https://files.pythonhosted.org/packages/18/2a/52e29c02047a493f15a78c0502bde4d3fab7c19c7813944d367cd501811c/grpcio-1.84.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:158c1c11cfb61b4849c3caf4d52de6f5ecd376e14446feb4a90dc95a90d616f5", size = 7029927, upload-time = "2026-09-14T06:57:19.767Z" }, + { url = "https://files.pythonhosted.org/packages/0a/11/9962b313553647abb091943e0721e4a1662ecc63cdfe930abf00abcce47a/grpcio-1.84.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:a9383401d9f116f98cacd4eba6c505a6edb80ba65badfc8e8ed8ae64983bcc44", size = 7782268, upload-time = "2026-09-14T06:57:22.381Z" }, + { url = "https://files.pythonhosted.org/packages/e2/b7/14a9413cb7d4b2e782b4f79c81a918610caedf55138ab5916f5fdd4b002f/grpcio-1.84.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bd8ea8eb3817b226057cc1c0e7ec4b378dcda52043b972b6ff12b1152178967d", size = 7187959, upload-time = "2026-09-14T06:57:24.686Z" }, + { url = "https://files.pythonhosted.org/packages/ee/3b/6cc8e6aed8f23be40f52af341e5d4595ec3ec8d7572271a692b5c1212178/grpcio-1.84.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:756ea5c2da00fa65c930284892d2a9706828704ca3ba40b4c51c4834eb39fcfd", size = 7737554, upload-time = "2026-09-14T06:57:27.5Z" }, + { url = "https://files.pythonhosted.org/packages/3c/7e/6f61002a01802ca9675e1b3599c9b0f9f3cf168ded94ebacc02199309f88/grpcio-1.84.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:28d2609691da93051e998495108bbddd2a9f7a561253bae94828d81290f30c15", size = 8792681, upload-time = "2026-09-14T06:57:29.731Z" }, + { url = "https://files.pythonhosted.org/packages/eb/84/8bec1ae7e6732a9b435a394ddfdfffde46c2620ae0109823f7cce1a54455/grpcio-1.84.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:27b8b36200a9fbee6e120246f4a8a41657549107ef19fb2c819c4b2fd524f39a", size = 8145493, upload-time = "2026-09-14T06:57:32.672Z" }, + { url = "https://files.pythonhosted.org/packages/59/84/c8c7bd210d657288f18af06522f150f61e81ea14fd3c7c135beed697c5fd/grpcio-1.84.0-cp311-cp311-win32.whl", hash = "sha256:465eef3d17e59ad22a556fc0138f7c7c799df426734344daec42c797d49fda99", size = 4495596, upload-time = "2026-09-14T06:57:34.799Z" }, + { url = "https://files.pythonhosted.org/packages/da/1e/da99356b3b573af357d059753a47fba54f1ca1a9c0e4deccd0210cb7f4ba/grpcio-1.84.0-cp311-cp311-win_amd64.whl", hash = "sha256:f9a456bdbed52a01c9ab8423bdebab04a5363c78676edc55ab9b58bd13bdf9e1", size = 5259900, upload-time = "2026-09-14T06:57:37.067Z" }, + { url = "https://files.pythonhosted.org/packages/0a/c1/4c9a2e0e6b0aaf02781404cad2f79211f989f2c827cf672a4a48d1604d3e/grpcio-1.84.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:b5c6f20d657ae09ae4e30d9d3a21edd13f1219d58cc6f999b9d1bb63be9c1baa", size = 6415756, upload-time = "2026-09-14T06:57:39.345Z" }, + { url = "https://files.pythonhosted.org/packages/b1/57/131e7007bdee9acb77a8dbe8a16fa9fef75f88c1695242d8ee0993ac2d3d/grpcio-1.84.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:406583b4e8fb2282ebd392e12b963e601c1f82e07125a8c2cb5b144e7e024796", size = 12339195, upload-time = "2026-09-14T06:57:42.373Z" }, + { url = "https://files.pythonhosted.org/packages/db/d1/a7b7cda98fcab9b3d2916204a872d87371158a7a34e41768f524584fb64d/grpcio-1.84.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fbdbcd06986ede3ce584083b1dc2afe6808e8943e5cf50ad11183c03aceda25a", size = 6984468, upload-time = "2026-09-14T06:57:45.035Z" }, + { url = "https://files.pythonhosted.org/packages/19/81/c5be83e3ac9416f73c4c51fe1ea9c41a0c42fc3509e3505faa46f5046abe/grpcio-1.84.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:23e6e8e8a75cff88e0a793bfd3becea03a13e2763ae90c1ff573bc19ca5b429a", size = 7749432, upload-time = "2026-09-14T06:57:47.395Z" }, + { url = "https://files.pythonhosted.org/packages/a0/bf/258cd7c0a7ed92745dc93c31666d462d05b702807a689744bd49fb833bde/grpcio-1.84.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b44f0a0fc7bc6677d38cc80bca1a32814ce6c8f200fb8b3c1a61c9d77eaefbf3", size = 7156115, upload-time = "2026-09-14T06:57:49.657Z" }, + { url = "https://files.pythonhosted.org/packages/2b/4b/7f829418dbfcf91b875e55e2973f1059a95decb4f081313416317ef04ec1/grpcio-1.84.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:210e4c32f907045eb8158273e60c6ab69a3947697df6245dbda381f26c59485b", size = 7708010, upload-time = "2026-09-14T06:57:52.496Z" }, + { url = "https://files.pythonhosted.org/packages/34/f0/9932e2fec6a04205f8bf3f8f4d2020479dcdac88feb6f93822ed31bf0eba/grpcio-1.84.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a71d24f40b0cc6798feaa978c7411dc1135b7018e9fc0442db611c139bf58344", size = 8759980, upload-time = "2026-09-14T06:57:55.312Z" }, + { url = "https://files.pythonhosted.org/packages/2c/5c/b67407c6dbc480dfc0715f6eccdb1061e7c88d85f9a330a241d357a538c5/grpcio-1.84.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f6c972474ce691aca74e58d17625450cef153dc4760364cadeb167983ea6d589", size = 8124904, upload-time = "2026-09-14T06:57:58.569Z" }, + { url = "https://files.pythonhosted.org/packages/02/37/2bfdae2df8dfcfc0df619b628e0c7153ce703adae827243f44720322ccc1/grpcio-1.84.0-cp312-cp312-win32.whl", hash = "sha256:0d532ade4486dad9b302ffa4d4683d67561051c26d17c4023322845e9fa10140", size = 4478915, upload-time = "2026-09-14T06:58:00.714Z" }, + { url = "https://files.pythonhosted.org/packages/85/2c/309268b7b39f6deb2342f634841e105623a0b67982e8b10ec516782ff1c6/grpcio-1.84.0-cp312-cp312-win_amd64.whl", hash = "sha256:49717e857899f4136d7657bf5aded61ac479110a075438290923a4d86af7cd02", size = 5253534, upload-time = "2026-09-14T06:58:03.336Z" }, + { url = "https://files.pythonhosted.org/packages/5d/51/40f99701adb01d4e5316a2aaf13838da1a24d5c879cd8c95156d7c364454/grpcio-1.84.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:209414080da8c20af94df1395b635da52dd57b5edc9e917e1deca0dc1c4bb55e", size = 6427619, upload-time = "2026-09-14T06:58:06.025Z" }, + { url = "https://files.pythonhosted.org/packages/c5/4b/ed8e22a1237e6b2be6ef4f221d074a5b0e0dd8a0da8c944c04aea731f0eb/grpcio-1.84.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:e41c3993eee896c617dbd8a505085d28b6e84a0445ed9a1f40f95808473cf678", size = 12336549, upload-time = "2026-09-14T06:58:08.583Z" }, + { url = "https://files.pythonhosted.org/packages/d3/50/00165b05cd73f45996748ea67ce9e55d08936f2fea94a7fd8541cc2d0e54/grpcio-1.84.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fff5ef3fe1bba7d6147e5f19e01e5e122ac2c076486887ddcb8d42e663400fbe", size = 6989458, upload-time = "2026-09-14T06:58:11.884Z" }, + { url = "https://files.pythonhosted.org/packages/26/38/d0486230e684d916f97429a53041db88410e662a38f2a8d09e2d90375840/grpcio-1.84.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b8c62888c3e49debf37ad9773e3c02f77b0c1e811f8fb0962f2b6c3bbab5b97a", size = 7757778, upload-time = "2026-09-14T06:58:14.849Z" }, + { url = "https://files.pythonhosted.org/packages/da/56/548a643decb059ca244499c675ae2c13a15f523ba94592c2774bd80a13c1/grpcio-1.84.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:986e9751d416d7a6eaa2fecdac38da63153d63a4b340ba7d624889c490451500", size = 7159572, upload-time = "2026-09-14T06:58:17.87Z" }, + { url = "https://files.pythonhosted.org/packages/db/f5/42caac81a79ec680f1f7a8eaf7ca90d2f93936ce0c3a073141ba96757f77/grpcio-1.84.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5933a052946873d01a42119a05420d669bdca436aeba2d1851988ccb12b421c0", size = 7710547, upload-time = "2026-09-14T06:58:20.607Z" }, + { url = "https://files.pythonhosted.org/packages/57/a4/828ad990b2410fee0a55cc73aa1bf98eb5b911c54847374ef4f24b9e877b/grpcio-1.84.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e094dd21f077af8194923fc263cad872eaa1802bb0156fd7e5ae18e99cd86715", size = 8761519, upload-time = "2026-09-14T06:58:23.875Z" }, + { url = "https://files.pythonhosted.org/packages/d5/a5/1f91af098919eaf5d80d5a61126ad9fae074e5190c25a3014ce1d8d0d890/grpcio-1.84.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:08735e3d08d24ab3132cf87e2e5dea8746cabcc7d676c2b0b7362f195feef9d9", size = 8121424, upload-time = "2026-09-14T06:58:27.006Z" }, + { url = "https://files.pythonhosted.org/packages/8c/8f/77fd4a7a913b636785479922349c4cb98d94d05d15652e556b3ca0df6663/grpcio-1.84.0-cp313-cp313-win32.whl", hash = "sha256:70bb4ce8be0c5606bec259cbd7152374470396413b7863a658a08c849e6b29ff", size = 4477974, upload-time = "2026-09-14T06:58:29.528Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9a/1fa59ddbfc8898e5518d1447e46f771f387f0ed6132ad531395338e51a5c/grpcio-1.84.0-cp313-cp313-win_amd64.whl", hash = "sha256:b61692f0069b3eee2fc8a3a1b7f6c044df9e03fede6ce69b3ca832e1c39f26c5", size = 5255326, upload-time = "2026-09-14T06:58:31.781Z" }, + { url = "https://files.pythonhosted.org/packages/26/6f/e25ca89ca5b0b7b95464c907a5c21a77c0ac8c4ee1dca164c4dd8f153ddb/grpcio-1.84.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:026d757df86c5b7a41de8200b9a2cda454aaa5004cb0c7e3374c66eb82f61499", size = 6428207, upload-time = "2026-09-14T06:58:34.401Z" }, + { url = "https://files.pythonhosted.org/packages/cd/b4/6b76b429f3f9b901cdbc306c81364d708bc957f847a05cbd1046cd2d05d8/grpcio-1.84.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:3de427b05f244ba2c2a9bdc67e7a6731c8340811524ecc4435466549f8af1d17", size = 12342420, upload-time = "2026-09-14T06:58:37.416Z" }, + { url = "https://files.pythonhosted.org/packages/af/64/ac86d638ba7f73bee0dccb608ba551d4f63adf75151f00d2c43e46d3979e/grpcio-1.84.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e90e3bdf7b5eac005fef631adae9cafde16f922def207b80a7c46b253c18ad20", size = 6998396, upload-time = "2026-09-14T06:58:40.535Z" }, + { url = "https://files.pythonhosted.org/packages/4a/65/fa12e9ec9d7ebf8cc3e81428fa9e1ca0d30d22d546ce2baa4c64bc917cbc/grpcio-1.84.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:e88d304f094f4937bc27ec6a435e218a084168f11ec630c8d5d39b431d08d81d", size = 7757538, upload-time = "2026-09-14T06:58:43.297Z" }, + { url = "https://files.pythonhosted.org/packages/21/d7/94240c7fae121ff1f116dcf04a3b7ee0216a06832c704310363f72638d4c/grpcio-1.84.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:57dc36a5ab0e676f5f6e171de2917fd0aef73f32a9aaf23956bfe19997a30bd1", size = 7161480, upload-time = "2026-09-14T06:58:45.939Z" }, + { url = "https://files.pythonhosted.org/packages/23/c9/7033e95d4b344969818b09185721c7608b47fc2498d97b5e4eec4995dbf3/grpcio-1.84.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5deda5b4bf62769eb98c119cca43d40e1231e34846b19db5cdea821d446a2253", size = 7720191, upload-time = "2026-09-14T06:58:48.308Z" }, + { url = "https://files.pythonhosted.org/packages/95/22/b45df2deba81d55069076859480bae7109c9eec02bce5515c799530cc2aa/grpcio-1.84.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9bab4cf571653a8afffb83ce21aa27b51dfe629b526b7b6adec35491fe1fc2ea", size = 8762792, upload-time = "2026-09-14T06:58:51.068Z" }, + { url = "https://files.pythonhosted.org/packages/de/c4/3e1c3d6155c16b8737cc31d5b477d6cf1fc7cdd10d58320cf0ec9b446f42/grpcio-1.84.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c5559b492007dc09b4de9b95dab05f0b5e53547aad230cf07e46c7dd017a3be5", size = 8123299, upload-time = "2026-09-14T06:58:54.332Z" }, + { url = "https://files.pythonhosted.org/packages/56/fe/f4864de5b815e5ba18858771f99381a398fac14117f89ef5291ed43d3c4e/grpcio-1.84.0-cp314-cp314-win32.whl", hash = "sha256:2c024da73b296f040b8360e60bd73a659b230093684a438da0e1260f34cc724e", size = 4562560, upload-time = "2026-09-14T06:58:56.894Z" }, + { url = "https://files.pythonhosted.org/packages/44/03/640811d4d8c84f5e603995c5a9bab725223aa472cad9ca4286c3bbf1c3e3/grpcio-1.84.0-cp314-cp314-win_amd64.whl", hash = "sha256:800b7e00d92553313c0463c200087930aa78678ec1d528193aeb50906f55989b", size = 5394092, upload-time = "2026-09-14T06:58:59.61Z" }, + { url = "https://files.pythonhosted.org/packages/4a/1a/9e3d2c9f005f680f03308fa894b1db91d4ab3f0fe65ff630c69561e91e95/grpcio-1.84.0-cp315-cp315-linux_armv7l.whl", hash = "sha256:47ecf0d9b81d981f07b61bd89eced9d2582f5eaacc3aaa36ad27f81aef70a27f", size = 6428252, upload-time = "2026-09-14T06:59:02.597Z" }, + { url = "https://files.pythonhosted.org/packages/77/34/0bc9f52ebf091311651eeab3a452fb557985604a3088cb5406f4d6df85d3/grpcio-1.84.0-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:61386101ecaa096b694d0dd278caf99a56aeec78440cc17e918eef0b50f2d567", size = 12359488, upload-time = "2026-09-14T06:59:05.646Z" }, + { url = "https://files.pythonhosted.org/packages/93/0e/c31052712f241cb6ecae9c226fabd519b7f8c64a7a40bac27e9ca0405b78/grpcio-1.84.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f6d178ba6dc8e82976c184b65fddde172d054c17237993a3e083efe4f134d55b", size = 7019339, upload-time = "2026-09-14T06:59:08.76Z" }, + { url = "https://files.pythonhosted.org/packages/55/b9/b9b33ea4f1eb4cad28833cade604febf357385b5ebb0c9c7562d020e167a/grpcio-1.84.0-cp315-cp315-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:15bb76489e337fc492685c9758e2fd4d4ab516b901ad830dc5a91987decf00be", size = 7107974, upload-time = "2026-09-14T06:59:11.568Z" }, + { url = "https://files.pythonhosted.org/packages/0e/9e/799d4c45db91bbdcd8c54b3982932dbcf3d059f7ce67dca3e8540faa1ece/grpcio-1.84.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:82da34ae4f639c73ac46e521e00c0a49bf86f717b9fb1f405f133e98731e38dc", size = 7200036, upload-time = "2026-09-14T06:59:14.401Z" }, + { url = "https://files.pythonhosted.org/packages/45/dc/dcfdd13ada41aff9098f0c2c6f260eb7debbc88b84b7e5fcbd085165427d/grpcio-1.84.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:9b73836ba0e16fcbb57c31cf6cbc2907c8d8c790b83679df454b74bd15e0be04", size = 7742281, upload-time = "2026-09-14T06:59:17.348Z" }, + { url = "https://files.pythonhosted.org/packages/55/31/75eab2ec77b80804bc5e21cec99b57598e726fca6484cd3e8920a97639d5/grpcio-1.84.0-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:42959bd50dd660ffc3f2a9bec15a6da4f9aaa0dda555d59ff2d2e80b908456a8", size = 8113629, upload-time = "2026-09-14T06:59:20.584Z" }, + { url = "https://files.pythonhosted.org/packages/34/f0/fdcf6bdc1df9ca11679a1187bef8e6b81df31a2baae69497e17344f05ea3/grpcio-1.84.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:659728f20fc7a0933ed7b1945435e31014b97ab8a5a7edcbaa70da4794aeb191", size = 8152972, upload-time = "2026-09-14T06:59:24.523Z" }, + { url = "https://files.pythonhosted.org/packages/5c/cf/6720e720bfa80fcb1ace873f66724eb3c8b03bba2fa078a30c12cab3212e/grpcio-1.84.0-cp315-cp315-win32.whl", hash = "sha256:edb6f87fc60ff438557291501b3e16c7a77c3b01a52d782cf276dccc7c5dd89c", size = 4561981, upload-time = "2026-09-14T06:59:27.275Z" }, + { url = "https://files.pythonhosted.org/packages/7f/b9/69d8a709df225bc2e06e028e9465166b174c24b3da07cc72d9a5ddc63194/grpcio-1.84.0-cp315-cp315-win_amd64.whl", hash = "sha256:4119efa6519871719ad81f33bc95ab87857dcb1c5801f30a6e592f2c41164169", size = 5394757, upload-time = "2026-09-14T06:59:30.118Z" }, +] + +[[package]] +name = "grpcio-tools" +version = "1.81.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "grpcio" }, + { name = "protobuf" }, + { name = "setuptools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/b3/1c5951352d6777fd7f99a0ccee04617fdfd8a5dbf2918a1f58c8b2b280b8/grpcio_tools-1.81.1.tar.gz", hash = "sha256:a22a3870180927fdd84e2b27d079ef5b7f5f8c6110181b6736afc17a463481f1", size = 6236155, upload-time = "2026-06-11T12:51:21.235Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/e1/1fcf884902ae7255d8da224cfa638ea88a46d50f62a33d06d35c8960b029/grpcio_tools-1.81.1-cp310-cp310-linux_armv7l.whl", hash = "sha256:9b6ba8a72cfda576508701a7c0bbeebe6f6f9843320d4f12e74efd19ddccd965", size = 2586261, upload-time = "2026-06-11T12:49:21.447Z" }, + { url = "https://files.pythonhosted.org/packages/a2/d7/1815110b2d40ec99dbb0a7e6d7eafd591cd1f1e9bf9d3858cd9cf3ffacbd/grpcio_tools-1.81.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:ac47a9ea1224df8b653072614e6f0207e9fbfe63fdabaa5918a60ca5fc931b88", size = 5817509, upload-time = "2026-06-11T12:49:25.958Z" }, + { url = "https://files.pythonhosted.org/packages/23/e8/af99579842b5a555312fa782f32ce0f99bd35b2b7a1243294b2755468857/grpcio_tools-1.81.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eac4bb645ceff0c147cc720a40ae68f97427eaafb4968e866dd8fcc20d3d4831", size = 2634112, upload-time = "2026-06-11T12:49:27.937Z" }, + { url = "https://files.pythonhosted.org/packages/b5/cd/235ad56ac728c49c17e9218c4daccd5831e6ec7af94236bec0cc66c71c68/grpcio_tools-1.81.1-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:cc410b621dd85193766c12dca2e238696199a27a65d2b31b6f0a4c6c0043ff26", size = 2957950, upload-time = "2026-06-11T12:49:29.619Z" }, + { url = "https://files.pythonhosted.org/packages/77/3e/9103e8b4610597bf89db49eb112091c91bf5d63ddef2a951e11a4be05f2b/grpcio_tools-1.81.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b62d254c214faa3773eac709376ae25cf7abff1a76ba5fc4dbcd7b14fc4e4ae6", size = 2697765, upload-time = "2026-06-11T12:49:31.702Z" }, + { url = "https://files.pythonhosted.org/packages/3c/86/beb2a43fbb93570a2305696083f6736566301d957869f463308ec6839f95/grpcio_tools-1.81.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:bd0b68dc76b10b3384b9b6e9f59202b83dcaafd8098eb644759a69316686acf8", size = 3147588, upload-time = "2026-06-11T12:49:33.748Z" }, + { url = "https://files.pythonhosted.org/packages/af/4d/b0182d9948631cd837a372b6625cf59d6e335d4aab0f425d4b7306619074/grpcio_tools-1.81.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:a28d231455ab6e3558299f7d831a73c8be8ee6b7ec614ecf39eb50c0ed15767f", size = 3708798, upload-time = "2026-06-11T12:49:35.979Z" }, + { url = "https://files.pythonhosted.org/packages/23/9b/f452a189d399051d85cf82fe2f27a070efaa52512a2c5e3ae6ef1ae99a1f/grpcio_tools-1.81.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:82740248eb6f3b6a38988cb5e64adb7303af9ea5cb4197c8ed08c1fabc767440", size = 3366969, upload-time = "2026-06-11T12:49:37.911Z" }, + { url = "https://files.pythonhosted.org/packages/9b/48/0075cb4f6ae7db280f461de2dbba700b22ae62e351ae13e6e461cd6804de/grpcio_tools-1.81.1-cp310-cp310-win32.whl", hash = "sha256:801d9d8ab5cddf8f8e064225292f0713427011252a07828a6b54e2ed64d534de", size = 1008713, upload-time = "2026-06-11T12:49:39.791Z" }, + { url = "https://files.pythonhosted.org/packages/17/bd/7692bc698259e5645b68720e77e7b176d376f6ae0c9db8b5b750a02f1958/grpcio_tools-1.81.1-cp310-cp310-win_amd64.whl", hash = "sha256:3c8611d6e4e859ac5373422ef27c4b7540cf98c9991c9abc6722613ef72b13aa", size = 1174752, upload-time = "2026-06-11T12:49:41.43Z" }, + { url = "https://files.pythonhosted.org/packages/18/76/14ff87090199a36f914388299a1148d0734a20cea1b0ca8480bae1f373f1/grpcio_tools-1.81.1-cp311-cp311-linux_armv7l.whl", hash = "sha256:8161f398f957a376cae7385ea7c8684f439d460ef702b528912da3bcb31fc515", size = 2586251, upload-time = "2026-06-11T12:49:43.514Z" }, + { url = "https://files.pythonhosted.org/packages/87/a8/d5aa99de9d8b2dd2a8192c1779796eda8b0d0f1dd915422e0a8a61b80391/grpcio_tools-1.81.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:53ef76cc3b0493ff734a5e8c39d5b519e1822236fcccdfe7677c5e1efd767761", size = 5818063, upload-time = "2026-06-11T12:49:45.975Z" }, + { url = "https://files.pythonhosted.org/packages/ba/cb/2e9a6dbc6a514dd3cd264fb3bf9217937453a4d45dbc3ca6ca4ee34ba1a7/grpcio_tools-1.81.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:690e6dcaa8b8a7886ce206ba344e2127211597e1a1ddab73df9f3d80c8f6707e", size = 2634061, upload-time = "2026-06-11T12:49:48.13Z" }, + { url = "https://files.pythonhosted.org/packages/c8/2b/2ccd1a929e6c8ad84a0aa8d66ad9f615b4a8e79d9927373d86aa36b4ba2e/grpcio_tools-1.81.1-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:ad7a997c07bd345e84842e60561e7e2cc090ce6c4e1d2f0407e31b85b40fc49a", size = 2958029, upload-time = "2026-06-11T12:49:50.466Z" }, + { url = "https://files.pythonhosted.org/packages/e7/67/2da8cd312edc348f44f26f82096b25cdb7d2905cd786acc6bf777b169502/grpcio_tools-1.81.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b6bd163ece4535726e5292b845ed80ae9b2cae73ba091c7d6c66033c430e3857", size = 2698031, upload-time = "2026-06-11T12:49:52.292Z" }, + { url = "https://files.pythonhosted.org/packages/d9/ba/ad1680fbdf9317c4f1e54c37c96d1f422370df66ac9adbd175c7cb3531d7/grpcio_tools-1.81.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2baa7e735f35b2a648144c03348a126097b13e101d3c242d5edb6ac91437ccbe", size = 3147541, upload-time = "2026-06-11T12:49:54.43Z" }, + { url = "https://files.pythonhosted.org/packages/57/c1/57cd08eef293d713cb8935295e4f08d8f0013480b2ba3aad1af0271eb7ba/grpcio_tools-1.81.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1d602b410b2b2addc434cace9ce4fe2035974a3078228f98ffa049a5c90acc2f", size = 3708524, upload-time = "2026-06-11T12:49:56.544Z" }, + { url = "https://files.pythonhosted.org/packages/52/31/01ea8ca9c82fe2c79b5b594c3ae427d56699bc106b2d91caca129add8b10/grpcio_tools-1.81.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f8cb64f87c45ccca8234fa47e6b21f09e43801ff11b556deecb461b3b3e9f292", size = 3367022, upload-time = "2026-06-11T12:49:59.608Z" }, + { url = "https://files.pythonhosted.org/packages/7d/35/8140cd175602df3d17215cfb28a7ea55b7a67e2b872be76e1ee4af5c4df9/grpcio_tools-1.81.1-cp311-cp311-win32.whl", hash = "sha256:87b25ca0e27373a4a32a629a4ba976f5764b9887dd50d6fe017d38009a0363e8", size = 1008980, upload-time = "2026-06-11T12:50:01.422Z" }, + { url = "https://files.pythonhosted.org/packages/be/86/1bd29ab3c52457702b96536f1f208ab27695322d855f95c9666dfb713019/grpcio_tools-1.81.1-cp311-cp311-win_amd64.whl", hash = "sha256:204de03b539a4b08772c6553b92bcc112cbc965e0ac22f909f6d133b8ac33a8c", size = 1174840, upload-time = "2026-06-11T12:50:03.408Z" }, + { url = "https://files.pythonhosted.org/packages/3b/8a/824a9ca20bcdce8a568bb8c9f98bfeb7fad62129235e6d2ae7576fd1250a/grpcio_tools-1.81.1-cp312-cp312-linux_armv7l.whl", hash = "sha256:353b1fafcc739c31ed42271052709595b340d34f27c459beeb78a32938305bb5", size = 2585927, upload-time = "2026-06-11T12:50:05.671Z" }, + { url = "https://files.pythonhosted.org/packages/2f/35/e5f9f671378b1b89a896150d3e4fa2c6ec61a5e1e9e5107ce4c140ccc931/grpcio_tools-1.81.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:768f584c2423cbeb6cb6867817a39365b987ff16b8259a3adbc6546b9e303a4e", size = 5815665, upload-time = "2026-06-11T12:50:08.178Z" }, + { url = "https://files.pythonhosted.org/packages/c6/02/631b628e4072e988c669bd8f1b2406ef3c9a4cfcb2625bbf2a308a07b71d/grpcio_tools-1.81.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1680b35a84f4694401819ac4acac42dda6dbc7bb8fc74112fd1a60425a07adf4", size = 2635518, upload-time = "2026-06-11T12:50:10.391Z" }, + { url = "https://files.pythonhosted.org/packages/de/7c/2e3537e3ea3d1c0ddd6766cf6a7c62b487d89fb005713df2781d5f21483a/grpcio_tools-1.81.1-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:f64e665c8ec639278ecf009beb92cbdcc5994f617c1af3d58036e1f70b1423ec", size = 2958252, upload-time = "2026-06-11T12:50:12.677Z" }, + { url = "https://files.pythonhosted.org/packages/35/68/14013cb2942bdac354746b643b4c37dd91906da8dce00f41c616e88bf33d/grpcio_tools-1.81.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ba8f1ae82ad199f43448995715445cc623fb20d3882382e4be61f0da8ccb3f0e", size = 2698439, upload-time = "2026-06-11T12:50:15.017Z" }, + { url = "https://files.pythonhosted.org/packages/bd/45/000c14c0338a7ad36054b9f17ea41842deb7841c05c067dd36cc831bc0f4/grpcio_tools-1.81.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b7b6d1e986d5923751bfe2b5cca9c4cb3d5653446e4fa4aacd438033e2dc360a", size = 3152160, upload-time = "2026-06-11T12:50:17.3Z" }, + { url = "https://files.pythonhosted.org/packages/41/97/881930ca3967d2c8a95649bea8ebc991a7cf2331bc96679fd3600450dccc/grpcio_tools-1.81.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:7f208c207aca639dcb34648d3826c38d7cf3485118fb2065117e9fc4827406b3", size = 3710468, upload-time = "2026-06-11T12:50:19.479Z" }, + { url = "https://files.pythonhosted.org/packages/c7/b5/67baeba7366162652cdc1dbd962289accde07241bc8f42f6f02b305efcc6/grpcio_tools-1.81.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:724ecb69af63d2f6d4ccea3e6fa0ca110ed9c5824d48c2f887c631bbb03c1c3c", size = 3370797, upload-time = "2026-06-11T12:50:21.501Z" }, + { url = "https://files.pythonhosted.org/packages/f2/5d/34f2dce2125ccb107e32b57f5a9c1257edcc0793b0d2fef1e8b13a6bac3c/grpcio_tools-1.81.1-cp312-cp312-win32.whl", hash = "sha256:895a6782cec86beac71ccebb4b9848259c6f04a3028b8e42fa8d40cfe5146593", size = 1008453, upload-time = "2026-06-11T12:50:23.358Z" }, + { url = "https://files.pythonhosted.org/packages/8a/be/09da8256ec8d2a5ce8a1acc51cbbc4ca52a462d78ed3412778440a56502e/grpcio_tools-1.81.1-cp312-cp312-win_amd64.whl", hash = "sha256:0265fd1386b7458302f79542558345880d484f8fa92ae196c0c0268242c5f23a", size = 1174857, upload-time = "2026-06-11T12:50:25.685Z" }, + { url = "https://files.pythonhosted.org/packages/76/90/5faa8b26e03495e5117f93bef8293cbada4af136362745dad7d1813ef0b0/grpcio_tools-1.81.1-cp313-cp313-linux_armv7l.whl", hash = "sha256:3d604b4fd114b79ebb9f865bf3e04fd3ae93c704e1fad96f7fd03b0865c263b7", size = 2586071, upload-time = "2026-06-11T12:50:28.4Z" }, + { url = "https://files.pythonhosted.org/packages/e8/9a/85dc589fa6ae2439451eaa81a1578de31e29c676980d38bef7549b8a1f45/grpcio_tools-1.81.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:3389e705460efa3f3758141ba5520e6743b131c9576197c944fb9cbe49048126", size = 5813299, upload-time = "2026-06-11T12:50:31.295Z" }, + { url = "https://files.pythonhosted.org/packages/77/fd/c53994e58a837e6eefe48f53eb3492afc04f2b8af255df4adb37d14378f8/grpcio_tools-1.81.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8a17d8ceeb6a855fadf39f5171c80a382d97c4db98d5943eca553497fdebf84b", size = 2634668, upload-time = "2026-06-11T12:50:33.938Z" }, + { url = "https://files.pythonhosted.org/packages/34/32/de988e86688686a2117e7ce6ce9eff4f638c929bb55b0afe60d6fbd2e45c/grpcio_tools-1.81.1-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:43baf71dc60fd653062da2e95e95c73b35dd130be8f9fa3d544c3af3f808a290", size = 2957930, upload-time = "2026-06-11T12:50:36.726Z" }, + { url = "https://files.pythonhosted.org/packages/72/97/3f18a0ea32b5f809d21961dbd0bc382b589a4c3d501e3d67c345d5456ed3/grpcio_tools-1.81.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:136e90906af0df51ad929713244ba812d0dbb1844b4f467d5d86bdb054698f90", size = 2697760, upload-time = "2026-06-11T12:50:39.108Z" }, + { url = "https://files.pythonhosted.org/packages/49/c0/dbf5cbc877290ff7504a59959a8af4fdcfdaa1e84237948405ccf1aa82a6/grpcio_tools-1.81.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd6c3bf3ea6a61eb58c54368d72ada591f2a270f3a31a32e8536e773337e76d9", size = 3151456, upload-time = "2026-06-11T12:50:41.983Z" }, + { url = "https://files.pythonhosted.org/packages/de/ea/16fe2dc83140a59e5c0a0b9dc2693dd36bfaa6bd835724b4ec66a68eab7b/grpcio_tools-1.81.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c306c307f8f74cddc4056fdbb6f1da55de087a21120efbd02bd915daa5a52fd", size = 3710469, upload-time = "2026-06-11T12:50:44.596Z" }, + { url = "https://files.pythonhosted.org/packages/22/7d/df987d7d81e7ad2f7516d9e9d56ff29c54dbc6d8587e425688dca9a28e49/grpcio_tools-1.81.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bdbdc927be2e0ea13c32564a72ee31d712a716fb6f8c0d53d37a77d8277c272c", size = 3370488, upload-time = "2026-06-11T12:50:47.199Z" }, + { url = "https://files.pythonhosted.org/packages/ba/c5/5a63444d694ea47bf670138208f71830cc1759c402c8818092b28ab2dc5f/grpcio_tools-1.81.1-cp313-cp313-win32.whl", hash = "sha256:9d383724bcd67244b6def9e9164c640ee9380c0b7534ee7545a6fb0022a59afe", size = 1008229, upload-time = "2026-06-11T12:50:49.527Z" }, + { url = "https://files.pythonhosted.org/packages/00/75/3945e26d5c94ae6ed9be5caef73d4d66c47dc8cfdd7b4995efaf942754e0/grpcio_tools-1.81.1-cp313-cp313-win_amd64.whl", hash = "sha256:f3eb15849979ca7bb864ce81a74d68b0f225a7f111ed3fe212bfc08cf9812b10", size = 1174523, upload-time = "2026-06-11T12:50:51.755Z" }, + { url = "https://files.pythonhosted.org/packages/0d/08/e581ad42ae517a61172285047e4d710e2ac75f2f1915f7c91f284254e6d5/grpcio_tools-1.81.1-cp314-cp314-linux_armv7l.whl", hash = "sha256:7d168ea26390717d0462c0d0408331dc98a60fc7f7e6118afac9b73f5a66d87c", size = 2585944, upload-time = "2026-06-11T12:50:54.528Z" }, + { url = "https://files.pythonhosted.org/packages/78/c8/200d90ebad685af7eea5ff7e0360c504dd01ec053fe0f1f9c4abe3ea2d5a/grpcio_tools-1.81.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:43c528655b226375013036692d8db4cd59060c1f41dd62c77f4d17b69f6ce828", size = 5813492, upload-time = "2026-06-11T12:50:57.291Z" }, + { url = "https://files.pythonhosted.org/packages/2d/c0/60da2a1af37aa8eb47308cec24d9f7709a8976fdec3a53fd35b56b358326/grpcio_tools-1.81.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a9c6fcc68c9d5a208967bfe4fd3224d3c3be9a950c3e827e8f4b17e15c2dc555", size = 2634991, upload-time = "2026-06-11T12:50:59.767Z" }, + { url = "https://files.pythonhosted.org/packages/0c/7f/dede28b579ae9bf9079ba1aa913e8088d1dc0cdbe21c85caa22f0790cad2/grpcio_tools-1.81.1-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:a987c85dcbe1b32066d7acd46266d1a428aecbd629331bf5b853e74c835bf876", size = 2957913, upload-time = "2026-06-11T12:51:02.31Z" }, + { url = "https://files.pythonhosted.org/packages/4c/38/4de2118adb58ec7ffba65ec623b5836db769665c192517cbf187db3f6145/grpcio_tools-1.81.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a882382507bb5ec6d7edc9648053dfd3bc8f9285cde56a6fa9b9a83b4bd07f1c", size = 2697709, upload-time = "2026-06-11T12:51:05.016Z" }, + { url = "https://files.pythonhosted.org/packages/6b/e1/762ced51059e4f694fd337ecae491581d42a4e61dcb0415d8c5c60e6ddcb/grpcio_tools-1.81.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7746e508d4239a02f7e93638be5bc0ebb0120ddb796f7506aaae9d47a4599d97", size = 3151884, upload-time = "2026-06-11T12:51:07.593Z" }, + { url = "https://files.pythonhosted.org/packages/19/d8/9823090dc801e7229944874e7429c3b98e741ac778d8dc373f60240e1c43/grpcio_tools-1.81.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:fc3d2a41a7a4467fa03b391394fffada9291fe8feebc8679b526f6bc36942b25", size = 3710404, upload-time = "2026-06-11T12:51:10.172Z" }, + { url = "https://files.pythonhosted.org/packages/64/4e/4eae98d02148cb6f9f452f09942afba407afa6851e6c1fddc5ae9ec0b4ed/grpcio_tools-1.81.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:21bb3ba90e6d8df1ff663d4ee39a4e5b25a64e8ed4902476ca9ded0954d3917a", size = 3370525, upload-time = "2026-06-11T12:51:12.678Z" }, + { url = "https://files.pythonhosted.org/packages/e0/3e/2206e597a128da6a03a6106d2eaf2c3e72c7d80843d4be933e3a3d10d02a/grpcio_tools-1.81.1-cp314-cp314-win32.whl", hash = "sha256:3dca56016d90a710c4d9861bae793dc089c1430a90c79ce672e948ddb65fa539", size = 1030582, upload-time = "2026-06-11T12:51:14.906Z" }, + { url = "https://files.pythonhosted.org/packages/cf/f2/bbeef86c687225b7bbc7c0acdfbd25c8bcaa3f5b1c941db053e5c3d9e859/grpcio_tools-1.81.1-cp314-cp314-win_amd64.whl", hash = "sha256:cb08172b7b629e75cb33866928d319a3196540a725eaab628ba721007140f1af", size = 1207490, upload-time = "2026-06-11T12:51:17.598Z" }, +] + [[package]] name = "h11" version = "0.16.0" @@ -1577,6 +1848,52 @@ dependencies = [ [package.metadata] requires-dist = [{ name = "mcp" }] +[[package]] +name = "mcp-transport-examples" +version = "0.1.0" +source = { editable = "examples/transports" } +dependencies = [ + { name = "aio-pika", version = "9.6.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "aio-pika", version = "10.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "aiomqtt" }, + { name = "grpcio" }, + { name = "mcp" }, + { name = "protobuf" }, +] + +[package.dev-dependencies] +dev = [ + { name = "cassetter", extra = ["grpc"] }, + { name = "coverage", extra = ["toml"] }, + { name = "cryptography" }, + { name = "grpcio-tools" }, + { name = "pyright" }, + { name = "pytest" }, + { name = "ruff" }, + { name = "types-protobuf" }, +] + +[package.metadata] +requires-dist = [ + { name = "aio-pika", specifier = ">=9.5" }, + { name = "aiomqtt", specifier = ">=2.4" }, + { name = "grpcio", specifier = ">=1.71" }, + { name = "mcp" }, + { name = "protobuf", specifier = ">=6.33.5" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "cassetter", extras = ["grpc"], specifier = ">=0.11.0" }, + { name = "coverage", extras = ["toml"], specifier = ">=7.10.7" }, + { name = "cryptography", specifier = ">=50.0.0" }, + { name = "grpcio-tools", specifier = "==1.81.1" }, + { name = "pyright", specifier = ">=1.1.400" }, + { name = "pytest", specifier = ">=8.4.0" }, + { name = "ruff", specifier = ">=0.8.5" }, + { name = "types-protobuf", specifier = ">=7.35.1.20260906" }, +] + [[package]] name = "mcp-types" source = { editable = "src/mcp-types" } @@ -1702,6 +2019,187 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e8/3d/1087453384dbde46a8c7f9356eead2c58be8a7bf156bca40243377c85715/more_itertools-11.1.0-py3-none-any.whl", hash = "sha256:4b65538ae22f6fed0ce4874efd317463a7489796a0939fa66824dd542125a192", size = 72226, upload-time = "2026-05-22T14:14:28.824Z" }, ] +[[package]] +name = "multidict" +version = "6.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/14/95/989c1b5ca17b72128661530cd6e351a0a83cda9a4d6c036e9ed976c18931/multidict-6.8.0.tar.gz", hash = "sha256:5cd4637ce76312ba1e05eb9c5193fec231f64fee0944e135fa1e951242355b37", size = 122412, upload-time = "2026-09-09T13:57:57.967Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/ec/714516a7e0f0e05bd5f67402bdeac775e0a50b883eafca3cf21adcacc228/multidict-6.8.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c0fe437a6d2f36aac2b49517057776575b5bf359df314cca20d230a6e139c089", size = 85595, upload-time = "2026-09-09T13:52:49.534Z" }, + { url = "https://files.pythonhosted.org/packages/c5/af/13c6c983bb2a59a567fda78ecc42bc3328889179480cc14389213f6837a8/multidict-6.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f3a0a31189acf6703307397c6139ddabd734c20c5ef92649fc93e473df6615a3", size = 51300, upload-time = "2026-09-09T13:52:51.225Z" }, + { url = "https://files.pythonhosted.org/packages/6d/59/38746cd2837b3656247d841c28bcac821be312319c89ea126ec335077ff4/multidict-6.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c9c98d2f0126ba84cb45601eed97ff67ff767e19ae6eb3c31b02827b54d700e5", size = 50492, upload-time = "2026-09-09T13:52:52.527Z" }, + { url = "https://files.pythonhosted.org/packages/aa/fe/9b1b44d060692fbeef47e7f9d72f9cb9e9c2c2fb8381fe543419449f5b7f/multidict-6.8.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7bc7003991ebd368a20d05228137a37b3d3066751f3ea1e4f7b8efe8e752f2f5", size = 259705, upload-time = "2026-09-09T13:52:54.082Z" }, + { url = "https://files.pythonhosted.org/packages/01/6a/dfb3e47ab0efcab2ddae494c86d9ac1fd47c86b3ebf687b1d6bf72221178/multidict-6.8.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c2c5fd0fd39574ccd58e1a52565b341aff522c5c836f1b3eb7605c371e61f52c", size = 258089, upload-time = "2026-09-09T13:52:55.647Z" }, + { url = "https://files.pythonhosted.org/packages/91/33/abc20faf78cd7060d4f904f0e669cc521537b1a2a0f6f8ad84cf5006447f/multidict-6.8.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b1cd4d66ce894a45482e1ac2837c31d0bd447df35065e542b60055aa2d00404b", size = 235981, upload-time = "2026-09-09T13:52:57.291Z" }, + { url = "https://files.pythonhosted.org/packages/bc/79/fba4622994740f487c927d7331876b1cb7253515bda66d8fb7bc0a382879/multidict-6.8.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7a2573d0fd34f361a4a14e54d8cda3a91ac4e55fbf0d719698024f3b09c5b147", size = 269134, upload-time = "2026-09-09T13:52:58.726Z" }, + { url = "https://files.pythonhosted.org/packages/de/6b/518eb2f391c9e579afb08d67fa280c4037d3b61fe57bc1071e6b84306cd4/multidict-6.8.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edda19aff836ec515caafc09ea53d2ab144a041f09ee9a7cefcbd3ae4e976256", size = 271441, upload-time = "2026-09-09T13:53:00.108Z" }, + { url = "https://files.pythonhosted.org/packages/53/d2/6db1ce7dc516d4b9afbe679b0e7190b26a517b25ccd11122c8c27ed098a3/multidict-6.8.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e418ec99574ca24365ca96546af285c2b021a1a072478a79f0e3cc3b08837154", size = 259314, upload-time = "2026-09-09T13:53:01.574Z" }, + { url = "https://files.pythonhosted.org/packages/7d/b7/548ec8cb0e3be3c03519420de5bc3094418254d8efa599da7e39a567c585/multidict-6.8.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:11d71490bf4bbff1141b14b93af419ad68c56b60bea9277fcb3f94dcca4796eb", size = 243703, upload-time = "2026-09-09T13:53:03.067Z" }, + { url = "https://files.pythonhosted.org/packages/f2/20/2af67fcc8aa6ee8727ae9a29fbebc81bcce5f9d78f8a6c365638824ffb09/multidict-6.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e8e1e895e23818d343e4ae7dd95a0a556fdeaf8b471acf1c0a39b93c6f54d478", size = 254376, upload-time = "2026-09-09T13:53:04.514Z" }, + { url = "https://files.pythonhosted.org/packages/0c/2b/b41aa60b0a1021304e95444aa40e3ff7a2a31028a21dd1d93c628de5b87c/multidict-6.8.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:15db8e6cab5f4cc9241bc56e69fdf3452cf49c10ee3c7977c742e68a275b3786", size = 249211, upload-time = "2026-09-09T13:53:06.056Z" }, + { url = "https://files.pythonhosted.org/packages/5d/e6/ad76aa06f5aace4ab82e70367525ef9b0606333963d9e436273c574c10ad/multidict-6.8.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:8dc2d9c3a924ed14166e63650b2cf9f59e7821743bdd50b23802bd97ca09bde5", size = 260855, upload-time = "2026-09-09T13:53:07.528Z" }, + { url = "https://files.pythonhosted.org/packages/19/45/248ebbd3276a6c066e631f7a49c7c2c0e0774600a752144e64f0ae9af82c/multidict-6.8.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:f2fa3d3b1c933d4bcb8fd2018700d5e7235c52f2ab8c88d22286965c5c0f00f8", size = 266149, upload-time = "2026-09-09T13:53:09.097Z" }, + { url = "https://files.pythonhosted.org/packages/76/72/3d87c20cd944a1eb9bcb21928d3e39e758aea8b0e4df3defc82d081f6c7f/multidict-6.8.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d6dc7804c50fabd28644d4d18a4b20aad3681b3e64f3acd3182b330ca73f7a32", size = 239270, upload-time = "2026-09-09T13:53:10.502Z" }, + { url = "https://files.pythonhosted.org/packages/2b/ff/8a69b1ecbe25cfdbf5200167357dc01ecf3abb974cb473fbaac5d3724e79/multidict-6.8.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:0eca15d627e942ce186a935061f1568cc46c02e97c419c8da802df2be9f917d8", size = 262229, upload-time = "2026-09-09T13:53:11.944Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e2/a40b690a319c3a17e1ef3cad127a7993d69cec06c892c9000cf67329cf75/multidict-6.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d7e5ba0a0153e35fbce9c51df530c8b4cb0c3012b46a04ff9a048441a269c2ed", size = 255270, upload-time = "2026-09-09T13:53:13.412Z" }, + { url = "https://files.pythonhosted.org/packages/99/bd/0e72f5981012a66ebf8b97153de8ff299b947af12d62beb832626f7a9b0d/multidict-6.8.0-cp310-cp310-win32.whl", hash = "sha256:b7e62b8fc7bd6cad007b9f2e0ad9c8d4854c06350d5f51e1a439dd18b510ecac", size = 46896, upload-time = "2026-09-09T13:53:14.799Z" }, + { url = "https://files.pythonhosted.org/packages/0d/43/7f93a35715d1ce1d96a7aedb7bffa870206390c0e2ba7cffd9b8533739a1/multidict-6.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:dc911ae6152e455b16a2a1a626aa6cd612fa01efb9d0a4ab3f5cf328b911483d", size = 51492, upload-time = "2026-09-09T13:53:16.067Z" }, + { url = "https://files.pythonhosted.org/packages/b5/d7/518dbe7eb714f413a093ef32c99010411e228ff57fe3e1bfb2df80abccf2/multidict-6.8.0-cp310-cp310-win_arm64.whl", hash = "sha256:6300d5176647145ba1e22991c924fb29743e54b4d7b8bc85a0d3ec0e55e189cb", size = 48049, upload-time = "2026-09-09T13:53:17.3Z" }, + { url = "https://files.pythonhosted.org/packages/d4/37/90216392620b6ef8704eb0bc055141745de396121067e98f1f72bdac33c3/multidict-6.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1b8986d4313dcee7c932837d16a535f1840b827bac1ea7c5c4c80751d0423794", size = 85033, upload-time = "2026-09-09T13:53:18.786Z" }, + { url = "https://files.pythonhosted.org/packages/2b/bb/e01b8cf906479b2fa046e992b84a9d9f39c1ed4058acc353004cd9a04df9/multidict-6.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2622fe114c0bd66ca5c461859357587f5a5e35ee5ff49fc5643d1bc78dbb41c6", size = 51008, upload-time = "2026-09-09T13:53:20.044Z" }, + { url = "https://files.pythonhosted.org/packages/c5/da/35d70c920812d9ddc6f295f6426457665194335fbc35aa0b96716aba219f/multidict-6.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:26a7aafc992e78872e2c8c1f7248c0e01139cf9020a7781b0c064fa566832712", size = 50232, upload-time = "2026-09-09T13:53:21.356Z" }, + { url = "https://files.pythonhosted.org/packages/91/6b/4c988a7c0daa4fbffc6080ed3c37b3a67cf225ba1de69d10a19ca1dd8d0d/multidict-6.8.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:05c2e90c5289c5f7436ba2c25812a5fbdaa1c1bc11c8d8d3bbf64f5cd7c633dd", size = 271678, upload-time = "2026-09-09T13:53:22.716Z" }, + { url = "https://files.pythonhosted.org/packages/73/2b/22c7de8a72fc5c36390e8049d86d842b032ac7c87ade035a3dafb7df4ffc/multidict-6.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7941ef106ca1f2c62314a13c7ed913bcf49641f3efdc12864d588e17870920ac", size = 270283, upload-time = "2026-09-09T13:53:24.235Z" }, + { url = "https://files.pythonhosted.org/packages/ce/f4/c1428318f945c57c016ba690338af41f87f18a7d3a7ef3227b1440a2c169/multidict-6.8.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a5a7ee1217949ddd43c6b7bcf70d5c22193bb50e8c695386de5905325e93ce9f", size = 245306, upload-time = "2026-09-09T13:53:25.656Z" }, + { url = "https://files.pythonhosted.org/packages/8b/92/a37f7519fb32b0bf43b0540292effe60edaf0691959214b227795bd3d56a/multidict-6.8.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6b62b7e0025aa48dec11e125e655d1157985a5fdcec04b1ad500101ad072b891", size = 279567, upload-time = "2026-09-09T13:53:27.162Z" }, + { url = "https://files.pythonhosted.org/packages/51/fe/a93c2ce417401863cc88ecf6561577625c140990d412e37f347a1c03a144/multidict-6.8.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ea880d441be7c510106bc56064be39266d948aef94ad4955e8784690019a5d9f", size = 282526, upload-time = "2026-09-09T13:53:28.927Z" }, + { url = "https://files.pythonhosted.org/packages/f2/9d/6bb4f84fdd82acfa09dc312ac133e7f76cbf2370004447f2a90e65e5d63f/multidict-6.8.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ab83fdd8cf307353edba9c427c17a3a021c2522d690f5633dd9f72d28b48ccca", size = 272604, upload-time = "2026-09-09T13:53:30.595Z" }, + { url = "https://files.pythonhosted.org/packages/3a/97/df0a30a4d786d313f24b39cb96edaaa3bbfe83a0b309577c81a797783ec5/multidict-6.8.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3d1f48582686a0a3b81e9b43234766cc96697df72081af3f48107bd3f34d34e5", size = 250786, upload-time = "2026-09-09T13:53:32.15Z" }, + { url = "https://files.pythonhosted.org/packages/6f/72/e59a917680d00214ba41f9fec19a8bec48f3bdf62656bc4377f37ae30947/multidict-6.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:563661919f603374c40cf45ffcd25535c12b8954203569a2ab1cee5265871cf4", size = 265419, upload-time = "2026-09-09T13:53:33.943Z" }, + { url = "https://files.pythonhosted.org/packages/47/21/0eb8868982ff07c1a2faaef7502ee0be32ef247dc1bf27881c51e7f4b20d/multidict-6.8.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:2ba9933e8f35fe4a70f540b837254c4055da82dc3a9e500a8f95e61498083a15", size = 258934, upload-time = "2026-09-09T13:53:35.662Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a6/0586396716faf950c10ffbe733e4a57b4eeda9f7073e60c09bd4a05a766e/multidict-6.8.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:68d40b2bace413f3231f5729d3fcfb1837fd31c4907e241b5d43211bfd76f3c2", size = 273168, upload-time = "2026-09-09T13:53:37.131Z" }, + { url = "https://files.pythonhosted.org/packages/31/79/7197af20190d0d832be3b18582be46c224f64f5ba1cd35d32068d6af31ed/multidict-6.8.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a9e246f67ac038568b854ed7c5578e4c6af1f742359901a8fcc3603ff1358df6", size = 275884, upload-time = "2026-09-09T13:53:38.579Z" }, + { url = "https://files.pythonhosted.org/packages/f7/f7/af60573e25ffecc09e805464580d579338cf1a44332ab52757038435ed60/multidict-6.8.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:29b6e7bc4442a56cf8e0dc1cabf3fdc77cd533568d6829fc76a1effd2ce332ec", size = 246967, upload-time = "2026-09-09T13:53:40.172Z" }, + { url = "https://files.pythonhosted.org/packages/6b/02/4459f8c5025ab034d3d9af9a34bbde11319016cff2f327362c8ec43a80a1/multidict-6.8.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:82780eb8bf59e8fb25dd081fde6e058805045d6374a7f2f877effc826ca4434b", size = 272358, upload-time = "2026-09-09T13:53:41.868Z" }, + { url = "https://files.pythonhosted.org/packages/51/99/680d3522ab51a77094d31d7958c9f5989499a01ffbe5aebe11d25212b385/multidict-6.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2196ba6df392c3574acadd14ef87550f3611349c8618564de324b806a7a31cee", size = 267450, upload-time = "2026-09-09T13:53:43.646Z" }, + { url = "https://files.pythonhosted.org/packages/4a/04/d0c773805b0aea171287b01a82e4c28c59ef2c2d93e8047394765181363f/multidict-6.8.0-cp311-cp311-win32.whl", hash = "sha256:b8b7aa75146266fd3e2a2437cf69ae188688c04ab8665b163d4257b46c1e0c83", size = 46847, upload-time = "2026-09-09T13:53:45.161Z" }, + { url = "https://files.pythonhosted.org/packages/71/f8/1a959771a4dcd3224bd7bb40054f66b98ba5b20d6b74fd273f548f887e0a/multidict-6.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:b03ca066b47b18b205cc080dca6f76cbd159f8cdd33a02a0700164c13b37e463", size = 51549, upload-time = "2026-09-09T13:53:46.448Z" }, + { url = "https://files.pythonhosted.org/packages/64/7c/3a74b11599a9d8f3cfbb78b9c5cac3ff3cdc17278e4c00329c7c18dcaff9/multidict-6.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:54af1266710cb0f305127ae0b970aff8d208057f8a29cd6e1db99b0114947035", size = 48020, upload-time = "2026-09-09T13:53:47.767Z" }, + { url = "https://files.pythonhosted.org/packages/13/83/a4621577679149ea001806f5963f3fc687c391c1bd5217157be2278863f5/multidict-6.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:22a310ad37672a261e55a8b5e28d0ae08cfb68abb1f46418ccd19835c3b8e836", size = 84146, upload-time = "2026-09-09T13:53:49.163Z" }, + { url = "https://files.pythonhosted.org/packages/09/00/236b063f3e606055a3a9ba8faa5d40e6c688b059a58056b055f213476f46/multidict-6.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bb8c7da8c861391f7ae48e3593762be2dabe405109e01aec520fbe1a6d15d14b", size = 51049, upload-time = "2026-09-09T13:53:50.46Z" }, + { url = "https://files.pythonhosted.org/packages/91/9d/954b139bfa969855f2d4cb5ae7b7d44dd7106f754305b6e21a9068213aa7/multidict-6.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:10456943903744ae1249728161c96bd9d2f7eb5ee17fcc2ffda2dc32e1bb36c7", size = 49362, upload-time = "2026-09-09T13:53:51.878Z" }, + { url = "https://files.pythonhosted.org/packages/d7/8a/8774f5b3f6d5266ecd1117876e04b405f0f1ce19aa750b35a826efe6cfe4/multidict-6.8.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:658f5a1895b804423d97b22d06fc0d0b171c7c01dcc3aa9c8faf0c0e26a249a5", size = 278619, upload-time = "2026-09-09T13:53:53.44Z" }, + { url = "https://files.pythonhosted.org/packages/db/47/736080fec911ed9f2dd57ccab5a8145e4f17c4987de0bfc27bee20e4d170/multidict-6.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90c10b22860dbd09982d0b8993b66231a861bea2993d4a817ff35273f6ea285a", size = 283771, upload-time = "2026-09-09T13:53:55.048Z" }, + { url = "https://files.pythonhosted.org/packages/4c/d5/b7f41f59b0583f092602308a5e7c16ec5efd00d60214b22511e89a38dd19/multidict-6.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:088b04a66b3c1fce6fe4d771ec184a0426262d0b86709c908477b4ac7965df40", size = 262108, upload-time = "2026-09-09T13:53:56.631Z" }, + { url = "https://files.pythonhosted.org/packages/54/b2/a52dc06c6e2598672308e3d392fd85b837b23c25dda459bedaea84985080/multidict-6.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9caef53b20a105c0d66518a34be2f71b2783de8d091767575ef86f6ea422236d", size = 289899, upload-time = "2026-09-09T13:53:58.415Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/00cda7983f37d119b86f1f89d5b4cf771ecb6d0fedeb9a0971758d6d6d4a/multidict-6.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a5e1583c14775580da05641240ce0d93f36ce3ddef3d5083a827468b0bcfe874", size = 293025, upload-time = "2026-09-09T13:53:59.973Z" }, + { url = "https://files.pythonhosted.org/packages/c6/c7/4544cc02e45bbfac4d8788b05379bb360021fd8c53fa74b0f624126ac188/multidict-6.8.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:003a3bddb32915c3f67096ea41d24e53edf710edb65a1f5d0c70ab40b0e4d20b", size = 287410, upload-time = "2026-09-09T13:54:01.652Z" }, + { url = "https://files.pythonhosted.org/packages/43/1a/7abed90b8eba381842235bfa6f4d730204fd7deb374fc87e3ec9b2c2b4ac/multidict-6.8.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:35977263d9bf506dbc65349f63b3b8c91606d4abc110990945e3b94bc671319c", size = 255878, upload-time = "2026-09-09T13:54:03.366Z" }, + { url = "https://files.pythonhosted.org/packages/25/3e/73fae10e15fc4d711975337caff7e494c87de5d0189afe3518b21b945326/multidict-6.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e9dc7b4ff6ef184504b49ef9a4113d49a646653b2ce89f5f48c1f57cdf6ba081", size = 277831, upload-time = "2026-09-09T13:54:04.963Z" }, + { url = "https://files.pythonhosted.org/packages/c5/cf/01cfc81492933331147004861bdff201d8adeba8485ecd8f490e755fe7e8/multidict-6.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:887f9a975996032c686719eb7b3e1e7942fab5079c2b778bbd9afe9a9d78244f", size = 275096, upload-time = "2026-09-09T13:54:06.661Z" }, + { url = "https://files.pythonhosted.org/packages/de/59/e9a3773b17297fa1e38fd4b3c6f5f2f458380796be62eca7d0d77c250618/multidict-6.8.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:f3071e6515cc63714d014da8f738ae9fa3997c476203f3cd46de380c2376ed7b", size = 279803, upload-time = "2026-09-09T13:54:08.389Z" }, + { url = "https://files.pythonhosted.org/packages/64/9d/2d712a2605b3971908e3b4f5eb6f98c353d9991e106f684d0e08ae581814/multidict-6.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c5f3a2af441670d80ce5fdf13b6c1b421fc1fc7fc5182d58ac7486738bb2b742", size = 284595, upload-time = "2026-09-09T13:54:10.17Z" }, + { url = "https://files.pythonhosted.org/packages/58/6c/21aded8586e552b29892268c576e5745d1a894c5451c9866ca3c06b7ec50/multidict-6.8.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:31e8901637e20ccb3cf8f8848b5d0f7a00462bf5b34f7cf3dcbb2753b18e8b39", size = 252641, upload-time = "2026-09-09T13:54:11.811Z" }, + { url = "https://files.pythonhosted.org/packages/2a/70/56a415ae0a45e5eae2ec817d46aeb72a1ae777863621c85f1f39d329275b/multidict-6.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:13967dca8b2f33230a1427b52438326bb1c9101a1df22a3309ed3fcbbb3c96f0", size = 283369, upload-time = "2026-09-09T13:54:13.59Z" }, + { url = "https://files.pythonhosted.org/packages/08/7e/7b7cd611fd94bf2f6bd16244c50495867ba394d5baaf8e6e487d39494ab3/multidict-6.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad474c11d851b6fc97cb625e4822bc0cbd567fc07dc2602e28faec5a36b42bbb", size = 281653, upload-time = "2026-09-09T13:54:15.174Z" }, + { url = "https://files.pythonhosted.org/packages/33/4a/b19a5892ef2ef6c68ae278b4f1504b82e01037baedd92c55d37e55ecad00/multidict-6.8.0-cp312-cp312-win32.whl", hash = "sha256:7bb0dad75068fee80fcb60f88569722c199d8656a16706702dc6e3b786819c90", size = 47936, upload-time = "2026-09-09T13:54:16.638Z" }, + { url = "https://files.pythonhosted.org/packages/29/00/1952f9f282aa71e7c3db3a6b47afb689d0ddf283dbded7e6326a91d421c9/multidict-6.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:7d26dc8f070c0ec5579e987fa615ffd6883086106eefdff9e10d160fc5630630", size = 51723, upload-time = "2026-09-09T13:54:18.05Z" }, + { url = "https://files.pythonhosted.org/packages/49/b5/c9d57dbafe25b8f3460ce2961c968539a81ff7a70160c44dcfd4255cbcd1/multidict-6.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:e6ec7d37841609a691b96a10b4fde386c7cd93ebbb939f59c9f23325ee788395", size = 48492, upload-time = "2026-09-09T13:54:19.42Z" }, + { url = "https://files.pythonhosted.org/packages/84/1f/d7112c2dd7db02677097be72fb65542f51a5aa73cb472b87ec211ba9e0dd/multidict-6.8.0-cp313-cp313-android_24_x86_64.whl", hash = "sha256:ec0a4d066356054d569a66e0a94691a2058b680be5e710298f61db11a3c4609f", size = 54197, upload-time = "2026-09-09T13:54:20.814Z" }, + { url = "https://files.pythonhosted.org/packages/ae/24/876015abbcb4a179d946579eb77b778eb5a948fc8381bc7928ba895bc051/multidict-6.8.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:714597cb5d5e15a8a449d2ae23c45b486a9e8fa33c462c7a33d7f35b65d92943", size = 47787, upload-time = "2026-09-09T13:54:22.51Z" }, + { url = "https://files.pythonhosted.org/packages/06/c1/ceb7d25f8a567599db2eb19b08cac58d67ff553cff42dcadbea9aba56a20/multidict-6.8.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:e0db3a4d1e264e225037a6023888972c25206a96e016021a5bea41c9a939f2a9", size = 48815, upload-time = "2026-09-09T13:54:23.986Z" }, + { url = "https://files.pythonhosted.org/packages/18/e3/e1c6e9c3818c34b782f23ce5fdba3eaa34ec6750dc53078dfac80fa59be7/multidict-6.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:27747162712e85c84598d364425dbf1714ff335bdb6ba3171c4e5081196e8916", size = 83484, upload-time = "2026-09-09T13:54:25.674Z" }, + { url = "https://files.pythonhosted.org/packages/4a/a0/c23f78a4badee9a5b3e760495c661c62a92c340a1dfd00f829cd16e256bb/multidict-6.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:847d6082ae694dc95e548acb201bc100e1cfa96513bc71fdcb86f709dad6c435", size = 50763, upload-time = "2026-09-09T13:54:27.135Z" }, + { url = "https://files.pythonhosted.org/packages/c4/fe/db552d402a3f6b650f5d3ae11b82b93833836aebb51bcda22d8691121129/multidict-6.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:88a6df88567680504ae28bfa7a1f2f64243d91e79a40b2c92ef42efc531e23da", size = 49029, upload-time = "2026-09-09T13:54:28.483Z" }, + { url = "https://files.pythonhosted.org/packages/01/b4/546853fba19dcef77cdf91fc173faf0b02284a49106cf250511166b4ec5c/multidict-6.8.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:560b211fc3bd4a1e1c6de44f6d38113bf5b410dfc89a4c0d2a3c0edbf1a0dfb8", size = 278863, upload-time = "2026-09-09T13:54:30.145Z" }, + { url = "https://files.pythonhosted.org/packages/ee/3f/4b52dac7db547936eb762123ac1d99df23f92fdb358bae600e322f611247/multidict-6.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:202436df907c15adbb94360296c425ea53cf8968a5d2cff9b5b9790ae1972b33", size = 283915, upload-time = "2026-09-09T13:54:31.937Z" }, + { url = "https://files.pythonhosted.org/packages/fd/6e/c0dfbf170e49a91bcb9ce850d51cb98357f3033c5227529200ca7625853e/multidict-6.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c46a08bf070d6849fed483e9d9833f9d06aecb8382ed985be0b38508b3ae958e", size = 260704, upload-time = "2026-09-09T13:54:33.529Z" }, + { url = "https://files.pythonhosted.org/packages/91/02/56973a060ab8dfc2e80bb6797682f6577aff7123cdb1de1a568670ae3499/multidict-6.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2cd560498ae8e1bcc955643c1d78eb8e338226d07a983c656ea8c4443d3eec0f", size = 290243, upload-time = "2026-09-09T13:54:35.408Z" }, + { url = "https://files.pythonhosted.org/packages/d5/67/69112989f131bdea4a87b74e82cb0a2daf37880cd92b0e6f0420020adceb/multidict-6.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:758233648ac47b07c575224c4eadd73c8929c3b4c31e2afcfea935fde1cda735", size = 291131, upload-time = "2026-09-09T13:54:37.205Z" }, + { url = "https://files.pythonhosted.org/packages/c2/75/9435f68b0cfc442d4917de85c26f2b2e1292630883414a25576083fa2469/multidict-6.8.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:122adc7c46ac1e31ecfc7f81b2530533dccafdba70f5d741649f87e336c63384", size = 287551, upload-time = "2026-09-09T13:54:38.835Z" }, + { url = "https://files.pythonhosted.org/packages/13/08/2ee4838081d6587849611aa7ec722c4cb2469e912fd0eaee980e7bac064c/multidict-6.8.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8125e60f3c70e323ac07dd8b3635f7b3bbc5c3a9ac04ae5988f668ff7ae28a18", size = 254591, upload-time = "2026-09-09T13:54:40.806Z" }, + { url = "https://files.pythonhosted.org/packages/94/f1/05673b51191f77f4198b8e4b35f16ea71c0300c72ca8aa027a66a61b6edc/multidict-6.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:83ff054b04915be5c15680da6c6012474a2cc2bf534129a0e8c6a99f17ba7238", size = 278204, upload-time = "2026-09-09T13:54:42.672Z" }, + { url = "https://files.pythonhosted.org/packages/45/4f/b6cf74322b3fbd3e011a1e903730191922291a7779f6d404114c2189b806/multidict-6.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:930c6058047410e3edff445f5a6e4457f2e089042dede00e2d18ce06f3ceae2e", size = 275600, upload-time = "2026-09-09T13:54:44.348Z" }, + { url = "https://files.pythonhosted.org/packages/1b/ab/958bbb04377159ff03c7314cd9d8a48dd6fc4f78c840589c22ab155ee9c7/multidict-6.8.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:13e26f59f0eecfc5f67c663ad550ffdaf62c0f657547cde387f6c86af1c9449e", size = 279793, upload-time = "2026-09-09T13:54:46.086Z" }, + { url = "https://files.pythonhosted.org/packages/a0/3a/706605ab0dfc4179748ee7949829e63c6f14ae28667aceeefaf2c701807f/multidict-6.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fd789a294d8e098528be29b2669b83005ce569339f8cef167fc0274c3115c34c", size = 284751, upload-time = "2026-09-09T13:54:47.793Z" }, + { url = "https://files.pythonhosted.org/packages/b3/a5/567e36c013ad023546de633079c6b22101dd43226b193cba00e6399703be/multidict-6.8.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:3126f2a96704505aa4e92a72d6e8a5d7f29d40a987ced8bf69e29d71dfc71fbc", size = 250812, upload-time = "2026-09-09T13:54:49.509Z" }, + { url = "https://files.pythonhosted.org/packages/0d/5f/6b0b64aa0cd346b07831dabaa6ccda0e73014c5df044b68baa763f0f0552/multidict-6.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:23c9ee89967b6a9b4048acb3b93b660ed714ce9c8bf3bbe652959bc120dc02dc", size = 281606, upload-time = "2026-09-09T13:54:51.288Z" }, + { url = "https://files.pythonhosted.org/packages/31/8c/b846b6796f26d496efb07fedef2b69f6de533da32a56f12d236722a96157/multidict-6.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7a62e302fc8cd6aa8972207e7e951d1fdee7c1dda18568305041d19f0e2c00f5", size = 281733, upload-time = "2026-09-09T13:54:53.05Z" }, + { url = "https://files.pythonhosted.org/packages/f0/f3/bf14a39d4af5697fd9404baaf70a0aeeb82d258b95de5cb16b1a7f98ae6f/multidict-6.8.0-cp313-cp313-win32.whl", hash = "sha256:093167d22a8c95af30f597b8a5686f20a14512989942d4be804d119899caca20", size = 47738, upload-time = "2026-09-09T13:54:54.676Z" }, + { url = "https://files.pythonhosted.org/packages/19/0a/598511a5741a3cb374971b3b02eda8a09896118ba528a54795f7e7e8bfb4/multidict-6.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:f25b61a708bd276e8cbb6afcbbf1b8e793a3be70ba0a842d0b8692020f83b706", size = 51609, upload-time = "2026-09-09T13:54:56.38Z" }, + { url = "https://files.pythonhosted.org/packages/fd/b7/6f5c1bd4ffe42d4a6db0f2f65491d4088e9c25c990358fb31a614621d664/multidict-6.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:bb36381e1f9f9d06eba2f10bdd438e5d20c07d5b55e1a3eee30b9f44cbf52316", size = 48280, upload-time = "2026-09-09T13:54:58.03Z" }, + { url = "https://files.pythonhosted.org/packages/ab/85/153341590e233a967c1d6791a83402d01693dec0f4c1f695606ef16c7ed2/multidict-6.8.0-cp314-cp314-android_24_x86_64.whl", hash = "sha256:f8b09b25e0f4dc2ea9e2adbb1cc3ba11a94d6fa3dd978ae659c8743052e1afbc", size = 53758, upload-time = "2026-09-09T13:54:59.563Z" }, + { url = "https://files.pythonhosted.org/packages/fd/ff/44f72d516ece0398683ef52061797d83a74b16b8c1e4587408e97959d783/multidict-6.8.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:1f57c414be82490bc0e0305fdb834186229b2d9b6a35fa0afd1eb1a772d125ab", size = 47495, upload-time = "2026-09-09T13:55:01.382Z" }, + { url = "https://files.pythonhosted.org/packages/50/5f/6e118f761b024dd35d26c2fe7ba41572bb0e8ac5f8cfccbbcbc2ff76da4e/multidict-6.8.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:00be37bde741bf60871082cd347a093218c44886e99231b7516671c70f2c280d", size = 48540, upload-time = "2026-09-09T13:55:02.989Z" }, + { url = "https://files.pythonhosted.org/packages/e8/4b/3eed744491b32f0e318e7db89dc06858732362f706e8d045fa9ab51a343a/multidict-6.8.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:e37b744849fb631bb52e3dadde35ffeee365a6c41cf71257b5b7acc9cd83fd38", size = 83130, upload-time = "2026-09-09T13:55:04.554Z" }, + { url = "https://files.pythonhosted.org/packages/6d/de/95c2c0ddcccb9a41ffbaa5df8ea059a8ff81916b7617a8847ecd89ed8061/multidict-6.8.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c2b2a96cf1dd99fe7867be4c013314225f4d5786e6685906e29932d42aca6f11", size = 50574, upload-time = "2026-09-09T13:55:06.387Z" }, + { url = "https://files.pythonhosted.org/packages/f5/b7/f4f4989594f99bc121ad9277090c4e49819b08ab1a96e132b628a9e10b7d/multidict-6.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:bea7df027015856ba5d0a88e3b4777ff8cb5c66b58fc108050fe79d4dd9d4d2d", size = 48786, upload-time = "2026-09-09T13:55:08.131Z" }, + { url = "https://files.pythonhosted.org/packages/b2/86/f1d86a0222f31fb3df8eef3d6c9abf7e8d65d49edd8d0d7e7afaf23d23cc/multidict-6.8.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d3da668e903c934ed0b587ecacfed6901f6ae6384a6e975887592b61845e78bc", size = 276670, upload-time = "2026-09-09T13:55:09.803Z" }, + { url = "https://files.pythonhosted.org/packages/03/50/6945c50f86a978b2bcace9ca344165ff80883be47d984489bbba8fa0ab20/multidict-6.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:64eaeda36ee8d88f9e8616a587a8c66a663283cf6e0dcf013c1ddd8c758e4aef", size = 279339, upload-time = "2026-09-09T13:55:11.685Z" }, + { url = "https://files.pythonhosted.org/packages/ee/2c/e649889ba23fd1f4442a85427b99d9e6261226b2ac31914aa7f5b241d947/multidict-6.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ac746cb365bac1c462da9e3e6ab8904a8efe2217a56b0b2e3d9480f41d2b2602", size = 252549, upload-time = "2026-09-09T13:55:13.527Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f8/1023b66e011b1395fb160dabb0f0608ef67e569f0bdb2c1d5ac9b2f2adc6/multidict-6.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:18f0e06360c3e451a3ab800355773c8d125a758238d780c800b0ee5e90ee903c", size = 286203, upload-time = "2026-09-09T13:55:15.19Z" }, + { url = "https://files.pythonhosted.org/packages/7e/6c/48aea545cbda6d0444848ec23d988c13b86538a00a1b7d3868cc2382ff94/multidict-6.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:69708fecaa88bcb2341397b49fc95057a835b02a3670c551b37f95dd79e64e3a", size = 285039, upload-time = "2026-09-09T13:55:16.928Z" }, + { url = "https://files.pythonhosted.org/packages/68/2a/066123b17291671bf67d2a5c65ee81a48de53913bd1b1578791519eacdb0/multidict-6.8.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9606f583e7acaf61e7b3f56074e14037b9af7cb194590edfc0114b3ae5931ff7", size = 281075, upload-time = "2026-09-09T13:55:19.155Z" }, + { url = "https://files.pythonhosted.org/packages/47/20/4f0b2c485da2e8a659cc677717a3745872918c9c85064491a1ef75d7a3bf/multidict-6.8.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1f66fe6a021173d0d47968491791966b9f3e6d61115f2491744aa0c07a6e67af", size = 250431, upload-time = "2026-09-09T13:55:21.07Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c7/b9a288901577aa0b82c33c64d52246c88076d260ad7b6c16b021ca0f8e99/multidict-6.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:be007d1aee2cbd530347dcafedb400891a3b5f1bd7135f95cf5d5b330b5219ee", size = 273891, upload-time = "2026-09-09T13:55:22.887Z" }, + { url = "https://files.pythonhosted.org/packages/da/51/0ba50cab2cfd067988de2abb73f23076ac727fe18d03f1368a59def64727/multidict-6.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:8457aff3c12a89a8e1c4674de5c777857fbc429f40fe117a3d29538547cbc364", size = 265262, upload-time = "2026-09-09T13:55:24.77Z" }, + { url = "https://files.pythonhosted.org/packages/0f/d6/e5be1117dbca6eb9ce231142b7e20599418bb3500147db51bf844ce8afcb/multidict-6.8.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:95c27b4f3f04320fc44e338573f40c5c956b504a7fcf081a157fd0b02579311c", size = 278033, upload-time = "2026-09-09T13:55:26.67Z" }, + { url = "https://files.pythonhosted.org/packages/d2/28/cad0afaec3caa56ea2c1ceed43c164d62ad3e83e950daf0d0c87bcf9dca7/multidict-6.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:d244cf6b52b5ba1c34c3832f4652a668ebb36d95949b96eed9a1c54d916a90dd", size = 281717, upload-time = "2026-09-09T13:55:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d2/025702df0b69b856db70a4d66f77622f51c3d99771ec9a07f3ca80f7e098/multidict-6.8.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5bbbb696c8024475b1877d14ce20d5f1cc05b8f6d786cea0fe3aa7fedc02e891", size = 247124, upload-time = "2026-09-09T13:55:30.497Z" }, + { url = "https://files.pythonhosted.org/packages/b4/96/9dddca563f06a921956389c0bc9b894355b98b0bdf62299e2560c50afb6d/multidict-6.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:cbd86f9787c5e2f5fd27d8b21458222f107347c6731c4e93dde68f554b466a2d", size = 275954, upload-time = "2026-09-09T13:55:32.57Z" }, + { url = "https://files.pythonhosted.org/packages/ec/91/8b2f1f2a774a955665f268340a2b59db7020c5f12baac02ae9ef1b1660cf/multidict-6.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2f8a4b0b4d639d525928c7f30de527bfdf9ead6e44a5e8cb9c50aced5e4590cb", size = 275508, upload-time = "2026-09-09T13:55:34.368Z" }, + { url = "https://files.pythonhosted.org/packages/b6/1a/e2cabdfc0880a61a99d2b8bc361035036fb5a2c6af31ea3fa054ba1065c5/multidict-6.8.0-cp314-cp314-win32.whl", hash = "sha256:8890c89d662560e51c55ac1304d6f919b23942abe9ae1127cb1de9aa6132fa52", size = 46938, upload-time = "2026-09-09T13:55:36.057Z" }, + { url = "https://files.pythonhosted.org/packages/b9/7c/11234bcba62c22a58f2ba168499cfe3531f49de3edd5090d04a8c6cdc936/multidict-6.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:45cc39ba50fb0754a4359b90f8229ae08598fe2266abe3521b4e5a9ba916534a", size = 50291, upload-time = "2026-09-09T13:55:37.698Z" }, + { url = "https://files.pythonhosted.org/packages/ab/61/793668439df924752a8137d6db0de97ed1add494779b01e4764dfc60571b/multidict-6.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d0264f8d5cb0a803f650a6a8572dfa0cd1e099a2234c588dc8fb220b415b865f", size = 47622, upload-time = "2026-09-09T13:55:39.335Z" }, + { url = "https://files.pythonhosted.org/packages/9c/b8/3c091b929e6b5b2f6e0eba2232178e76d4503c8b96b92dfc281ff1d823be/multidict-6.8.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:1969971900b0871530f9b62280dcc2d75688e74d2a69262bc01faf2b96c78f04", size = 88789, upload-time = "2026-09-09T13:55:41.086Z" }, + { url = "https://files.pythonhosted.org/packages/9e/d7/3df83fab22dd64615db71e3b3cc1346b581d1459719637ce52144f9f6558/multidict-6.8.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:8180b635290a75af8478f1b3e9810135381ae24833293fe77b85c1c21ff842ab", size = 53399, upload-time = "2026-09-09T13:55:42.685Z" }, + { url = "https://files.pythonhosted.org/packages/2d/78/41bd04c04b0aed16540c4856c9e012afc1c254298da154398308df05e26a/multidict-6.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:4261863fc8b5ab1b815ede94e592e94c6af5b04616014929057e61859e7382a9", size = 51597, upload-time = "2026-09-09T13:55:44.569Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6b/7bc4cdddf624e1e7e0231734b1331729ea46df10d7c8fd3fce79756e7d0e/multidict-6.8.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0b143d53590e89f43153d81d505a8448d4d57354354385aef8a51d67ffefa27e", size = 264391, upload-time = "2026-09-09T13:55:46.548Z" }, + { url = "https://files.pythonhosted.org/packages/dc/b6/d2a946e5938771e92c39354563e535ef6bc6dfe399dd4307c6df8dfea183/multidict-6.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:da1c112c5784ccd9d32cd90be6739fee32644e874eff6ae8f0497cba3e352e58", size = 264680, upload-time = "2026-09-09T13:55:49.915Z" }, + { url = "https://files.pythonhosted.org/packages/33/6b/3f9e981c42e7eb9329918523f0f9362ceb0ac3ee0ee1165c28f674249d75/multidict-6.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f7eefd0233a7c33ca980a5cfef26f1e9b5e2137839e752a99963696729f12d91", size = 235420, upload-time = "2026-09-09T13:55:51.92Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e1/a3a33a039fb6d381800ae5d1d587b697b8c27fcdfe48819420f08703acba/multidict-6.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:348bb85e2038b40c007383616d73f734869063772372519549ebd7da1723d1a4", size = 270309, upload-time = "2026-09-09T13:55:54.023Z" }, + { url = "https://files.pythonhosted.org/packages/95/5d/8b06724a957f2e480f159b9550988a67810fbe9555a09c5f6a2a4b829607/multidict-6.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:095f62ea4e7a3be2f6c567ab695ce10e950f2adb905c1bec82281593e0b2d2ad", size = 275169, upload-time = "2026-09-09T13:55:55.948Z" }, + { url = "https://files.pythonhosted.org/packages/ab/32/8f3dfe2ffa5d0df2a95f71e63c2f11fe3b5e1771f26ef73bb1af84de83f8/multidict-6.8.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:be569fff1d85cd29391c431c5641c8772acb75bbdc61e60a8e82fceb9023d385", size = 264900, upload-time = "2026-09-09T13:55:57.803Z" }, + { url = "https://files.pythonhosted.org/packages/6b/73/d5829fc00a055d6ab445e0876346ee9cdee670766cd4190dc0a496188c0f/multidict-6.8.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3533a03e4e789baf6a286e7b0b1b6da3f3d7c3eab569686ee29ee1d8b52e2cb4", size = 242486, upload-time = "2026-09-09T13:56:00.002Z" }, + { url = "https://files.pythonhosted.org/packages/b7/58/e8d7874038e31e0533182d1c3c5331a856b9c849a71bb26a21850e8c91e1/multidict-6.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1bdb9b8fba5a9aef673ec90db3f55b1ce743f2fbdea4d37dc04d14ccdfc153ff", size = 259916, upload-time = "2026-09-09T13:56:01.802Z" }, + { url = "https://files.pythonhosted.org/packages/4d/f8/1b56a7401acda20efc016440f4fad3bef66c4aee54ca080ec143881ebb0d/multidict-6.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:f8d7b66c9e09c0bb0add2b5895e646b62a0849e71155066f215523de6b95cbe6", size = 251209, upload-time = "2026-09-09T13:56:03.767Z" }, + { url = "https://files.pythonhosted.org/packages/bd/5b/68d67a9e302b0645a747ba910c30eb41f2834fcdc1d85f53eae2dfceee0a/multidict-6.8.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:563d6500ca80dac7bba6f48a78e0ffd87e21a7d4d24642c6503a2ddccd70c110", size = 264505, upload-time = "2026-09-09T13:56:05.795Z" }, + { url = "https://files.pythonhosted.org/packages/18/13/4dc304ba2c5f5307b474ab2ce1ed1f6b02b0b4e233c182e3981ed436c2e3/multidict-6.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:346ac52e56bcda320c0dcdfdd081947ed7cada33afea4e2284bef7b0733bff9b", size = 264916, upload-time = "2026-09-09T13:56:09.079Z" }, + { url = "https://files.pythonhosted.org/packages/dc/0f/7b1f729d18369915009185201be5d0b8df0e525340fe6a600d2f8441d6cf/multidict-6.8.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:4ee953a5ebaeed38dc21cc032ed17a9d9782802e00042200497ab4b01b0bf7c0", size = 236839, upload-time = "2026-09-09T13:56:11.273Z" }, + { url = "https://files.pythonhosted.org/packages/22/d1/eba1b88b18b7019d9136303fe77909257c40fabde5aaf138a4d900b6ce3c/multidict-6.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:2f79cc3e8039a8cf5c77e0811b0807953fd52d0863b9b76970b20d696dc64a78", size = 265307, upload-time = "2026-09-09T13:56:13.379Z" }, + { url = "https://files.pythonhosted.org/packages/aa/a6/6c1e4106faa27118ac612f4d664eaf909de252634785286262a627108e58/multidict-6.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:43a4b56555bbcf8af161e7c7682bd93eec10f068c95844511864c018c8e5e13b", size = 259041, upload-time = "2026-09-09T13:56:15.666Z" }, + { url = "https://files.pythonhosted.org/packages/c0/bc/ecfb8b6faa8e158a71b03bdf7f947f30e0bc5d899cc357573a76ab7bb1e5/multidict-6.8.0-cp314-cp314t-win32.whl", hash = "sha256:48ea524a25a1cd5972cf293bc95713918cba0bcd6fa9b992d906c857c546abe2", size = 50628, upload-time = "2026-09-09T13:56:17.837Z" }, + { url = "https://files.pythonhosted.org/packages/30/7f/e27fb699b70ad24dbd02ddee604658acb36f907c03c045baffe4ea774501/multidict-6.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d0be2b832435001bc623ca7f1499ca1a853d4f082fb61221a80ce71132f50b26", size = 55592, upload-time = "2026-09-09T13:56:19.652Z" }, + { url = "https://files.pythonhosted.org/packages/eb/7a/76de70b2f6733696803f1ee56abe44a3757a52777383032c7373d3fea0f4/multidict-6.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:62b8e291a4f7edbf7cde7a43d831d893ba443a1b627498b53581943b0e348feb", size = 50300, upload-time = "2026-09-09T13:56:21.516Z" }, + { url = "https://files.pythonhosted.org/packages/ce/32/4de7320ae032dc768090d11f708d2d386df3db04cb6b8b0db0230cfc66c3/multidict-6.8.0-cp315-cp315-android_24_x86_64.whl", hash = "sha256:e192018b732f7b168e6604cbdf40fa8e05c996693b9eb445a0d8a73f4b77c5d3", size = 53761, upload-time = "2026-09-09T13:56:23.192Z" }, + { url = "https://files.pythonhosted.org/packages/5c/45/ecb641309dc2cdc6040f18e22c68eb5e94398f9404c4365d810f4292e053/multidict-6.8.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:b25426f9f6ed402835617c8f23609a47045f91ecff365eb6734817e039a8ed25", size = 47505, upload-time = "2026-09-09T13:56:24.902Z" }, + { url = "https://files.pythonhosted.org/packages/eb/68/87d6161b9fef11943e0b894203da3fff561933ca3c9b2952b6e7100e9c9f/multidict-6.8.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:fa6c2880709c84457de104385b704fc28860f27e442ad13966fc4af8e714fe9c", size = 48549, upload-time = "2026-09-09T13:56:26.574Z" }, + { url = "https://files.pythonhosted.org/packages/97/f7/d852d2276407640cdbd29fe11cac6e93f70f59542cba174ef9d146738946/multidict-6.8.0-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:eabb03dc3e4ed6333ecd1cc9826ec80e7a98b5506deeb832d7260c8e44166d23", size = 83157, upload-time = "2026-09-09T13:56:28.227Z" }, + { url = "https://files.pythonhosted.org/packages/14/e3/16fe7ffa6090591d83cf6bc2486e77ce891705fb6d0191823140928311b5/multidict-6.8.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:59e539c4eb4d3a53b0e630a6ba2b2f2824732b5e73f90e30a280f12fde157b15", size = 50578, upload-time = "2026-09-09T13:56:30Z" }, + { url = "https://files.pythonhosted.org/packages/d0/0c/e38e41c1087a599f86ff58a01f358abf7c4db3c26a3e90eebb3e02193ef1/multidict-6.8.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:835d5a90b11d1f5f8200ff3cc8316bded76eebebc92436398947a27657e645e7", size = 48815, upload-time = "2026-09-09T13:56:32.056Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f0/eb691f42af8e7775992f57904ec75dc356fc7cdc896e5f30879decdd26f2/multidict-6.8.0-cp315-cp315-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d2d236b8a44ae91536a12ebcb996bdb31cf27425f36b4d05c87f2ba2716050ba", size = 274804, upload-time = "2026-09-09T13:56:36.741Z" }, + { url = "https://files.pythonhosted.org/packages/87/05/28472ccfeb43c00a043c0385ca4294da21a5957859fb7860e2ebdb3e3011/multidict-6.8.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bb9a60b7faa5d37c426fa91cf4d6738182a1f2755b9fab7c9c64cd466c4ce51e", size = 279693, upload-time = "2026-09-09T13:56:38.531Z" }, + { url = "https://files.pythonhosted.org/packages/f3/a1/2b4fe73e5fecff807b47650a155c391a103136428cb21d6ba8e39c5912b5/multidict-6.8.0-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0ef606c15cac6c90279acf34120784b6f36662cbf382defd3955cd8f1115336b", size = 254969, upload-time = "2026-09-09T13:56:40.407Z" }, + { url = "https://files.pythonhosted.org/packages/44/e0/c97d1822783dfe52e02fd150fa3f02eb22410211a9e2615f71541803ed4b/multidict-6.8.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:68186a2d4051c8ffd17be33553bea2ec9bbc8ef860fe2980a221d96126296f31", size = 286392, upload-time = "2026-09-09T13:56:42.234Z" }, + { url = "https://files.pythonhosted.org/packages/2f/d9/772f1339e1d051236bcc137b0eac2b4aaaa0bbb56aaf924e9aaba901d9c1/multidict-6.8.0-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2cc66abb85e2108c9ff8a1c0d20fa260bf690bbb33caef4ff3ecb2c2cbdfff5d", size = 285348, upload-time = "2026-09-09T13:56:44.255Z" }, + { url = "https://files.pythonhosted.org/packages/06/ae/cd045747e4680362e02955a82c468e95b5e4d319e3a79574b3fb677de568/multidict-6.8.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:69b3e519a132bb943b0daae15fc8c2168706b17f826481d32a32a5e784b129e3", size = 282721, upload-time = "2026-09-09T13:56:46.088Z" }, + { url = "https://files.pythonhosted.org/packages/35/14/0802d9a3aae4ef21eaa39adbd729a380fa095932105e1424e417b53e783f/multidict-6.8.0-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e41226ecf607f062fe34a2f4cf64ad3a89e3a0180dc800b463b6b14c06dd10dc", size = 253168, upload-time = "2026-09-09T13:56:48.07Z" }, + { url = "https://files.pythonhosted.org/packages/b1/64/3f92298bab8fbe1332e708863fb55b66e755be6f416b3459720d48b33af9/multidict-6.8.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:610c7637bc36b90f39e6c66f710f93d57018f83d53e1e187caaa218c6892b95f", size = 274209, upload-time = "2026-09-09T13:56:50.023Z" }, + { url = "https://files.pythonhosted.org/packages/08/c2/2001ac0eac1a8b7390a5902d7115f66d4f256268057a502200b6ab12dad7/multidict-6.8.0-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:65c85c79f5a2c04fbbc18f006c014674dc5fdf270cb978d8862c82c6f694e60c", size = 268044, upload-time = "2026-09-09T13:56:52.033Z" }, + { url = "https://files.pythonhosted.org/packages/0d/90/78a9e26c85f89abd562a67f7fcbaef9007fd5c37bb9efac19f1cf604e7c2/multidict-6.8.0-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:628ff11e6720f90acd0c305dfa3339f04a783a20de8cda6ac333ba46447261e8", size = 274806, upload-time = "2026-09-09T13:56:53.975Z" }, + { url = "https://files.pythonhosted.org/packages/3d/71/713bd445421b21531234c1f3630b768192cb9d80c8b1c5b05c5b505ff4c0/multidict-6.8.0-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:0935971bffd0b479fc90c4811ca787703e93fcb6afea939a375dfc80285ab368", size = 281890, upload-time = "2026-09-09T13:56:55.848Z" }, + { url = "https://files.pythonhosted.org/packages/de/a5/1387c538663e2dc8c27bbc7cd6955cb66de0f55c780cf7cd0fc06a1a16ca/multidict-6.8.0-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:9442b14eec262a1f74369bbd07e75bc5155105164649a4b9fbc1ebc7b8fb0b14", size = 249749, upload-time = "2026-09-09T13:56:58.01Z" }, + { url = "https://files.pythonhosted.org/packages/91/15/104296c9d70896b9759ce0812aa4899fab76d8b16bb32dcc5a78ab547c89/multidict-6.8.0-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:397599503b718f0137f26d3f6532d6955069cd2e5917c47ef581495bc2529ff8", size = 276138, upload-time = "2026-09-09T13:57:03.591Z" }, + { url = "https://files.pythonhosted.org/packages/f7/0a/f2a0c2658e9d7ff5964ec2820a02054558636fafd663230ddc8310b8ed39/multidict-6.8.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:9e37024b41d7a7e7e9cce14b248d54707c21c2a2ea30a47b71bdcefcafec00f2", size = 277077, upload-time = "2026-09-09T13:57:06.024Z" }, + { url = "https://files.pythonhosted.org/packages/98/50/bc46566caffba5c1c4a510519156371edf7c4ecd35c9ef917d0c1803487d/multidict-6.8.0-cp315-cp315-win32.whl", hash = "sha256:071da134651b04a8507dfb331ac0988f376337c2aea59486bf20989fb5b5a64e", size = 46930, upload-time = "2026-09-09T13:57:08.009Z" }, + { url = "https://files.pythonhosted.org/packages/6f/1a/cafb31049ecc1a6ce52bcc69fa436cca239adc057b1718a0c49044848663/multidict-6.8.0-cp315-cp315-win_amd64.whl", hash = "sha256:3bafff8598f0528017ddc74194e5451d5c22d046c98935f8f86247b0f286e4f8", size = 50294, upload-time = "2026-09-09T13:57:09.986Z" }, + { url = "https://files.pythonhosted.org/packages/6b/51/00e037da14cd1d894b123e0bbe62de5c561679a6ab23ab1c009f2965dcda/multidict-6.8.0-cp315-cp315-win_arm64.whl", hash = "sha256:e886ef8c9879105fe4fc99417447b3a5f35d1131412ce839470bd2089fe2043f", size = 47626, upload-time = "2026-09-09T13:57:11.738Z" }, + { url = "https://files.pythonhosted.org/packages/52/f7/aeb947982197e8b4f5c4da3961ee473ea5a050b94a6ff3b88baf64621401/multidict-6.8.0-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:883284137e25318ed9735b742ae46341a864888fae28e8b6314c4f84da080f08", size = 88801, upload-time = "2026-09-09T13:57:13.957Z" }, + { url = "https://files.pythonhosted.org/packages/35/d8/593948c016c3f850e3cd56a4e0144151eb409d2b8690f0c0ce7f7d33dbea/multidict-6.8.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:ca52b9ec80851366197577154c862c4c4c7036ca76ae94cef5cb59c5cfeab944", size = 53376, upload-time = "2026-09-09T13:57:15.94Z" }, + { url = "https://files.pythonhosted.org/packages/58/b9/097a05bca533027c0477b6a90bf927dbbb4b23cc9090bbb37a2e972af8d5/multidict-6.8.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:91fa75d0a693832106d98f66c849f034f21c828d14437f1fb97d3784aab89e84", size = 51629, upload-time = "2026-09-09T13:57:17.685Z" }, + { url = "https://files.pythonhosted.org/packages/fe/07/938ed21967f12380d0b8861645fb65a942f3669e31d5163ed94d23103b61/multidict-6.8.0-cp315-cp315t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:307c1acd812fe897e7fbe10c6758822e8c04be4e7c60a9f54901cdf8b5ab8bc3", size = 261967, upload-time = "2026-09-09T13:57:19.752Z" }, + { url = "https://files.pythonhosted.org/packages/89/e8/e66bf843fd29c01712dde9edeb9f4ad0ffab06ab4ada4b721ad7bc73b3d5/multidict-6.8.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5caf684986a2490628f059a99dd107b566a2d34cf947f8eb8387e0500a1f90c5", size = 265923, upload-time = "2026-09-09T13:57:21.784Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f8/e9be849b225af28a8eee2c6bfea23594a777c753fe97e2ff7e2180c8935a/multidict-6.8.0-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:77745725125d01fd613b6db043362aa7c6bfbfdb23d45dbfc3d92bf58160af62", size = 239380, upload-time = "2026-09-09T13:57:24.3Z" }, + { url = "https://files.pythonhosted.org/packages/ab/67/4dbad08f5081978c591afae9e836ec9ddae90e9e76be6d6ce10757483dc4/multidict-6.8.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8daafaa0b2eb43f76898ced78b1e0fb91b38c4fa50da516c18067f2a2d578c20", size = 271591, upload-time = "2026-09-09T13:57:26.611Z" }, + { url = "https://files.pythonhosted.org/packages/92/3f/e9c97222d7e104e54e556f118ec7d091ab41a0c10c630f2b97e5b43f5404/multidict-6.8.0-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c68e0c0649d17c2d0339e3674e86a4aeba4a7e6b21c1e394cf947a95433b31d0", size = 276091, upload-time = "2026-09-09T13:57:28.997Z" }, + { url = "https://files.pythonhosted.org/packages/26/ca/728e7ce05ac9c0303554e7162e74d91fe49e65bad7dfbb377f783dd32c0a/multidict-6.8.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d8a5ac357ac283490a8d1899b0383355fd1f8634b14ba0d59e4c0dd97db85556", size = 266493, upload-time = "2026-09-09T13:57:31.256Z" }, + { url = "https://files.pythonhosted.org/packages/8f/74/7c658d2769863af16fb7d7c6be50b29659892a06a632858863eee3a31842/multidict-6.8.0-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:46029e6e27a3ec0dc55b53f58df82d10f04c5e111f78248279b530bedad2c30a", size = 245302, upload-time = "2026-09-09T13:57:33.464Z" }, + { url = "https://files.pythonhosted.org/packages/2d/2c/d4350a20a0e8c66a447d694e8713438262665203fe826c3f4e385f052b72/multidict-6.8.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:8d1046b5427dcafe6e8a0e07527dd74f1ee694006160162f53f3a17f15aad3b4", size = 261016, upload-time = "2026-09-09T13:57:35.651Z" }, + { url = "https://files.pythonhosted.org/packages/1f/78/83df999c8beb72a012cfac42f2b833c4a48f8e836fd4407747b355a2430e/multidict-6.8.0-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:f1f4a220db6ed7c8fd16b6d644ffd1f082651693204daf3275e049fadc849e39", size = 255021, upload-time = "2026-09-09T13:57:37.758Z" }, + { url = "https://files.pythonhosted.org/packages/fb/13/f2c0a2dac6d91f74aa124f3e9f07ec497ceae5ed2df2753d249601cd7262/multidict-6.8.0-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:029897732a9c798737457e382bf84e8c64237eff224a90aea2639f4413c45e4e", size = 263066, upload-time = "2026-09-09T13:57:39.897Z" }, + { url = "https://files.pythonhosted.org/packages/59/1d/730008d4639ace731bbb1399e1ac13cbdf506f7d6fb861d75044ffb3994d/multidict-6.8.0-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:29be9fd289e9ab8f480996ea2f686e1654b80242033843cb11691688329423f1", size = 266510, upload-time = "2026-09-09T13:57:42.39Z" }, + { url = "https://files.pythonhosted.org/packages/ab/ca/bec67a5d206dc5748e50c93f6f71deec14305c3657cfe250c3887caf7839/multidict-6.8.0-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:29631224698de1e42abc8fa7658d830e0aed0029785144b5832b695da5adef2f", size = 239423, upload-time = "2026-09-09T13:57:44.304Z" }, + { url = "https://files.pythonhosted.org/packages/9e/db/5f153fe51fbac7d80f3bb8bd6fab8db8b6cd061e7a11371676dfed3712bc/multidict-6.8.0-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:962f18c59a000f30b084ea2e6b8001521bb315efd4e5f10acf9fb36f366b7882", size = 266902, upload-time = "2026-09-09T13:57:46.297Z" }, + { url = "https://files.pythonhosted.org/packages/89/0f/9efca48a351551de4dc0c183f523109dbe87c645a4732d5f1c70b4880dca/multidict-6.8.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:c60e50bc5b07faac92fd3a20fa21cc8cf3e3f7204d2867b206c73293ebc19101", size = 260887, upload-time = "2026-09-09T13:57:48.268Z" }, + { url = "https://files.pythonhosted.org/packages/df/d8/bb879a62e0809448e53f6237e71670066ecf3bbc5896a7a6705b6628d86a/multidict-6.8.0-cp315-cp315t-win32.whl", hash = "sha256:fc5460940f50dff00731b4132366840ba9685286ea88ea104b661899084f3fea", size = 50533, upload-time = "2026-09-09T13:57:50.31Z" }, + { url = "https://files.pythonhosted.org/packages/fe/62/3e5308d8871636e4b9620e4b3acfcf2b5caf79b19d317690ec13f7fc8b57/multidict-6.8.0-cp315-cp315t-win_amd64.whl", hash = "sha256:b367c342327717d644db4c0ddb37ceb655c84822215ea0773a3a36911b74b71d", size = 55572, upload-time = "2026-09-09T13:57:52.301Z" }, + { url = "https://files.pythonhosted.org/packages/b9/cc/d3c10e10ee3bb7a7b4abbb3157306b2ce7e0018c9c2d16b32b468739d2b7/multidict-6.8.0-cp315-cp315t-win_arm64.whl", hash = "sha256:0c1c4debad7337627b86837abdf0237ca3cb3d7e17de7eab0177c263878546d4", size = 50322, upload-time = "2026-09-09T13:57:54.099Z" }, + { url = "https://files.pythonhosted.org/packages/b1/ee/be4e1a4b7a2b27f4fb6936510d4bebcb41b0562c946930ad26916e069cf9/multidict-6.8.0-py3-none-any.whl", hash = "sha256:75daa15ca16d6285eb2e104b2f05ee6f8d9836c68da3ce5c85f615a0450eed0e", size = 16297, upload-time = "2026-09-09T13:57:56.106Z" }, +] + [[package]] name = "mypy-extensions" version = "1.1.0" @@ -1838,6 +2336,40 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, ] +[[package]] +name = "paho-mqtt" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/39/15/0a6214e76d4d32e7f663b109cf71fb22561c2be0f701d67f93950cd40542/paho_mqtt-2.1.0.tar.gz", hash = "sha256:12d6e7511d4137555a3f6ea167ae846af2c7357b10bc6fa4f7c3968fc1723834", size = 148848, upload-time = "2024-04-29T19:52:55.591Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c4/cb/00451c3cf31790287768bb12c6bec834f5d292eaf3022afc88e14b8afc94/paho_mqtt-2.1.0-py3-none-any.whl", hash = "sha256:6db9ba9b34ed5bc6b6e3812718c7e06e2fd7444540df2455d2c51bd58808feee", size = 67219, upload-time = "2024-04-29T19:52:48.345Z" }, +] + +[[package]] +name = "pamqp" +version = "3.3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +sdist = { url = "https://files.pythonhosted.org/packages/fb/62/35bbd3d3021e008606cd0a9532db7850c65741bbf69ac8a3a0d8cfeb7934/pamqp-3.3.0.tar.gz", hash = "sha256:40b8795bd4efcf2b0f8821c1de83d12ca16d5760f4507836267fd7a02b06763b", size = 30993, upload-time = "2024-01-12T20:37:25.085Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ac/8d/c1e93296e109a320e508e38118cf7d1fc2a4d1c2ec64de78565b3c445eb5/pamqp-3.3.0-py2.py3-none-any.whl", hash = "sha256:c901a684794157ae39b52cbf700db8c9aae7a470f13528b9d7b4e5f7202f8eb0", size = 33848, upload-time = "2024-01-12T20:37:21.359Z" }, +] + +[[package]] +name = "pamqp" +version = "4.0.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.11' and python_full_version < '3.14'", +] +sdist = { url = "https://files.pythonhosted.org/packages/31/4c/33a0ddaaac7bc42f9a542dbaaee8b580ceca3f89bf5da7c498d1fa97ff9a/pamqp-4.0.1.tar.gz", hash = "sha256:9dd13b828e346622793981f14a5df817fce5de998c746209d6c0154eb8403970", size = 137192, upload-time = "2026-07-06T16:37:51.732Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/14/1dfc08b743ba995a38dee0ea09beb46a05c7fe8ac53d729095905f7bf11d/pamqp-4.0.1-py3-none-any.whl", hash = "sha256:a547f45128b06e42ce8d7a739b0cfcc40f2c724770622eaaff4a3f587b1cf7d0", size = 32773, upload-time = "2026-07-06T16:37:50.623Z" }, +] + [[package]] name = "pathspec" version = "1.0.4" @@ -1959,6 +2491,151 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] +[[package]] +name = "propcache" +version = "0.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b3/9a/9fbf4e4ec0c2d7f1c32519fff782ef467859b8faa9fbc5331a96f6395d43/propcache-0.5.4.tar.gz", hash = "sha256:ff6b113f50bc066a698db5d944d2c6dc7507168dd3341e255a8892fd0715a558", size = 61545, upload-time = "2026-09-16T00:17:14.386Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/dd/997704118aea215cc5f65c277f5323657b4ba44c1f9a32fb11c8064e4997/propcache-0.5.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b77c313314524ca9c38fbd70f73515d04597ac58c40c939bc0e71eeb4abff680", size = 87225, upload-time = "2026-09-16T00:13:43.864Z" }, + { url = "https://files.pythonhosted.org/packages/3e/6d/ceeca1762ed51230c08d557591404f844ecef5e294550136155572f7faae/propcache-0.5.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8f911c395cef73c510bac566da9507bb6a43e7763d0c79138dc60ee53f11207e", size = 50809, upload-time = "2026-09-16T00:13:45.33Z" }, + { url = "https://files.pythonhosted.org/packages/ff/9a/6be90814d8762952594a9e380161802541bff690f3a2e011dcc28ec193ce/propcache-0.5.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d83b12902eb8bce151259c86c03ba746600b2d994543de46e370cecf96c452f2", size = 52552, upload-time = "2026-09-16T00:13:46.48Z" }, + { url = "https://files.pythonhosted.org/packages/bf/d1/7fd3ffb5ca0e669a8fbf55d9fbb17c50ff5f44becfefcab27a9b9b67908f/propcache-0.5.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9281e922c072158c91974d4589f1dbe0fee6d467f284c28e463f9f5a4d933f4", size = 226804, upload-time = "2026-09-16T00:13:47.739Z" }, + { url = "https://files.pythonhosted.org/packages/74/87/a3e199c45b26587f073db655d19c30d2144b0f883827ba6ab77acf5c7d45/propcache-0.5.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9f3551b8a35c1df3e7ea4d2d86edee15f0dde1bddd434a71744048683544d0ef", size = 234452, upload-time = "2026-09-16T00:13:48.98Z" }, + { url = "https://files.pythonhosted.org/packages/12/7c/08c15c7df256f94a6b4563f74165c3242fb554cdd1909b661d093c31b976/propcache-0.5.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ec6a85f424afa8d23e0d9a094e5dbb6eda01da91c92b9183cd433768247ffc97", size = 240399, upload-time = "2026-09-16T00:13:50.416Z" }, + { url = "https://files.pythonhosted.org/packages/f0/51/5adee15e12a7e314cece4543fbf295b0fe1b504dc78b32dff4c8eb1e29b0/propcache-0.5.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f574e460d1c8a08384a016fdb09ccf3543433263ed6b2f97104f979e64ea57c2", size = 224251, upload-time = "2026-09-16T00:13:51.874Z" }, + { url = "https://files.pythonhosted.org/packages/9d/53/54bd510bb5d473edf66914602885226187218b4e3a5017e9dd80bceea41f/propcache-0.5.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d8e017eeb7482bed34cdb0d61cf2bcfc88d104bbab296a17cd16a6af8aabc70e", size = 202721, upload-time = "2026-09-16T00:13:53.524Z" }, + { url = "https://files.pythonhosted.org/packages/6f/b4/a468700d0526dfb2ca6af5d790125de5894acc7d8ecd99243ecedfa4c4cc/propcache-0.5.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f273dcf7149a50527c4fd1f55cfe9eac0f60753f5af544b4c9352578e20c0874", size = 219148, upload-time = "2026-09-16T00:13:55.068Z" }, + { url = "https://files.pythonhosted.org/packages/6b/d4/bf563e19ac9a5cc47113431337fdf2ee3573859f3f3e0a58e59833e9b75d/propcache-0.5.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:fc2461ecc45f17893f8207e73b46ea8ba93e33630e51cf4af3fbc21d47462b1a", size = 211011, upload-time = "2026-09-16T00:13:56.588Z" }, + { url = "https://files.pythonhosted.org/packages/4b/3d/0189b6537f4a8cd795e685a130a6f47aeda78a5b7b062574705cffc685ca/propcache-0.5.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:279655a16973f1ee2bd2fe79973137681642fd9ae0d89215bba263726eb0dc3a", size = 228148, upload-time = "2026-09-16T00:13:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/ce/b7/59c16e245a549df202b4ebe2de91fa58a67dc4373df9840e372a64224dee/propcache-0.5.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:e9f165403b81fea7e89c932d89046a1e3d9a3a60e8d7ef2f249dccdcb0982bf5", size = 201432, upload-time = "2026-09-16T00:13:59.453Z" }, + { url = "https://files.pythonhosted.org/packages/41/0b/7b19eb20bb0b1f9476d08f6e387ae094dc6870fc06d459ea1f35f28b25d3/propcache-0.5.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:1783582065a1f07f9d9ee1e992e13f15d7dc8fb1eb3a7476d43eb3f2e69d26bb", size = 228923, upload-time = "2026-09-16T00:14:00.731Z" }, + { url = "https://files.pythonhosted.org/packages/3a/f9/b1a0bd47218216b935d019912d6fec1676ae7c07f15fa82bfb54b8d58976/propcache-0.5.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d605bb239b796e82a81c6709548b2bd460ab73b4590cb0c83de8a2dd9694d0f", size = 218327, upload-time = "2026-09-16T00:14:02.091Z" }, + { url = "https://files.pythonhosted.org/packages/55/c0/51e5d1ad504e9529831606534c48e272db751072eee9ff07e4abe37e50bd/propcache-0.5.4-cp310-cp310-win32.whl", hash = "sha256:141fdbd73748db0cf7636035030aaac383d2efde8f34e7bc24594cc776d225b8", size = 43096, upload-time = "2026-09-16T00:14:03.427Z" }, + { url = "https://files.pythonhosted.org/packages/80/57/0a0b4b1122f4ed5bcd1c626d910003cdf5282c2a12f8a1cfa17970b3ded2/propcache-0.5.4-cp310-cp310-win_amd64.whl", hash = "sha256:146f48a9e4812611a7581003b1a39de56c34967046310c4171a68ef908c9a745", size = 46589, upload-time = "2026-09-16T00:14:04.581Z" }, + { url = "https://files.pythonhosted.org/packages/66/87/e71b24adc8ece61782ba3d6f3879e6a07fdb85bce21a9c529524184e3ec6/propcache-0.5.4-cp310-cp310-win_arm64.whl", hash = "sha256:6c7599df2b57ebeea8de011b5f2f7b85de95e76037d43d34b95e328430275487", size = 43982, upload-time = "2026-09-16T00:14:05.728Z" }, + { url = "https://files.pythonhosted.org/packages/1e/40/14b21e505b7921617466576423f188a5c9caddfdaa1cf4b2b8a83d8fe216/propcache-0.5.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:897d1ddf6716e8f47200f7aad9a0efa6cc7586df66c6defa572f9eab379c078e", size = 86393, upload-time = "2026-09-16T00:14:06.9Z" }, + { url = "https://files.pythonhosted.org/packages/e7/4b/5a52e1a7b43563f7d408814194bb23cc8bf214eb6b86639b667a33a8d0d0/propcache-0.5.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9cbfff4423eef4cc6cafc021469641a2b835f610b2647a6c5281903e21b8670d", size = 50431, upload-time = "2026-09-16T00:14:08.025Z" }, + { url = "https://files.pythonhosted.org/packages/05/cf/b5248180bf056cc76acc60c9c6e8c0ebbfdbd1c6cffd31fd14996927b7c8/propcache-0.5.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3fc24f209c1b7f7f688b66b98293954f5504279760999b58920ee12dd8471c1d", size = 52116, upload-time = "2026-09-16T00:14:09.114Z" }, + { url = "https://files.pythonhosted.org/packages/86/a8/7c6cd6bfead1a11f2e411e688640e6d26574cb0bde7dcaa7423b0b65ed7a/propcache-0.5.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:62530ca89187827e4a4fe733f971abe81a7542eeea48ff61995f19b64d7199c8", size = 238729, upload-time = "2026-09-16T00:14:10.357Z" }, + { url = "https://files.pythonhosted.org/packages/5c/b4/442715b2e980df51be52d203549279e027728f24c80b00b5e525e31cd5ea/propcache-0.5.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56fc3f7599528db40b1efa0889a620116e2704144495273d66066e8164e45838", size = 246121, upload-time = "2026-09-16T00:14:11.735Z" }, + { url = "https://files.pythonhosted.org/packages/bc/5d/df0684fc2b1732a01a7bec26d7897369022712422d25b09c37ce7dbc88a2/propcache-0.5.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4f2d880ff60f45898f4acfa152aac8d04e3ee627d90ff4003491bf92239d5757", size = 251735, upload-time = "2026-09-16T00:14:13.053Z" }, + { url = "https://files.pythonhosted.org/packages/c7/06/519a5ebb48b6f94beb48396e55c905f12246a25c3a3608a7ec7bceabf50e/propcache-0.5.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6e9368e87a3efc285e559131092c5db643eb8e56de4ee42064d5baec22ef2bb5", size = 235381, upload-time = "2026-09-16T00:14:14.398Z" }, + { url = "https://files.pythonhosted.org/packages/3c/07/1e0a9bb310830f2245edbd5cd3c6d24a783c053c4efd8e08e386e513c940/propcache-0.5.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:004e685b315646c410771836e72a44f143bbe624f29653a42687815069a303d5", size = 208973, upload-time = "2026-09-16T00:14:15.715Z" }, + { url = "https://files.pythonhosted.org/packages/62/5c/9324fab27d6088eecc47fe4332bf7aaf8c1ded93c36f558391e8a06d41a7/propcache-0.5.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:594eb4c6ec35e7179b058481f4e9f02521b56de16fa577c4b85c76fb1bf8a9f8", size = 233897, upload-time = "2026-09-16T00:14:17.25Z" }, + { url = "https://files.pythonhosted.org/packages/9c/a3/570d92fc952eae93b676f3a1568f4b89264102abd3c982ab6a9ebec58dcf/propcache-0.5.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:2dba2f02d2d5c09ef8a0e6c1a42aeaa451f4be9898cb00b04fe98717da2eb23b", size = 223512, upload-time = "2026-09-16T00:14:18.87Z" }, + { url = "https://files.pythonhosted.org/packages/65/47/26810d889d89bba31db397e6a88f8984af775f5ed6bad0a29dce84324cff/propcache-0.5.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c3ef2818d63bc86071e9d2989ae75a1bc32b8f7059cfd9f5abbbee70c32e2ed6", size = 239043, upload-time = "2026-09-16T00:14:20.366Z" }, + { url = "https://files.pythonhosted.org/packages/89/2d/f9c47691aa024c8299a3afacd78d22a01ab57eb627b481b6089708e71017/propcache-0.5.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:dd2ac8f5b643454c2cc6b6118b13da16e88f4a6434fc3ba61aca384029f04f36", size = 208218, upload-time = "2026-09-16T00:14:21.801Z" }, + { url = "https://files.pythonhosted.org/packages/74/6b/d510c0c378cabbf9d0ac7b663af6d00f2e9074073d93b20c85f24aa5c071/propcache-0.5.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:4054acf80d40456a0537f2913b349718649d8d6458a14ab7f48d0ce28c30869d", size = 240301, upload-time = "2026-09-16T00:14:23.121Z" }, + { url = "https://files.pythonhosted.org/packages/3d/80/c80f6adaaa1e51f0db2dce8c9b3714d94ec45e358a21f9a1910b10b40a80/propcache-0.5.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:40e94adb1e7d39ff28a8bd8d8b8fbd1df6b9f40976dbe379134f1ce058e532dd", size = 230785, upload-time = "2026-09-16T00:14:24.458Z" }, + { url = "https://files.pythonhosted.org/packages/d9/e2/c32a7df3f39caa7f11b2eb37ea5b6960a6946f2bc7c4b8ff97bbdf6d6b6e/propcache-0.5.4-cp311-cp311-win32.whl", hash = "sha256:9f86f7259efe2c951f43e57d471c9b41daa5bfc7db9f67189059cf1ae6d77fd9", size = 42747, upload-time = "2026-09-16T00:14:25.715Z" }, + { url = "https://files.pythonhosted.org/packages/0a/8a/3db6a3543d8101263b4c52978b6276a04ead2caff2c5ab880d934f47bd89/propcache-0.5.4-cp311-cp311-win_amd64.whl", hash = "sha256:e904d4d01f36bd6e197590be1533c44e06058771e0746dd073a8ebb3ef880858", size = 46268, upload-time = "2026-09-16T00:14:26.996Z" }, + { url = "https://files.pythonhosted.org/packages/41/07/5222e2665bbf6e45847492ecbf3b9f3e4975a0ae300e5fd465df7d48ce55/propcache-0.5.4-cp311-cp311-win_arm64.whl", hash = "sha256:d42a9a856a4a6e2f6c10f1318c07e7daa498d6593abe745c71dae4521a26ca39", size = 43547, upload-time = "2026-09-16T00:14:28.143Z" }, + { url = "https://files.pythonhosted.org/packages/71/cd/348d58f142aebc4873345c6b31087629182ca6e0f2b3caeaa528cf882eba/propcache-0.5.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b28f41fa3b8c6900457f858ec5b03998f3a6d535fbc1bb2edec5961ea05ec429", size = 87285, upload-time = "2026-09-16T00:14:29.362Z" }, + { url = "https://files.pythonhosted.org/packages/df/f4/f3ffaee281b276da854ac1d7a6a506d26cbc62ea2e623756f1d0a4a1ba1a/propcache-0.5.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:dcbf346a318a5e30063f547630b02bb787ce2f45b6368d5da143660b6a3835d8", size = 50984, upload-time = "2026-09-16T00:14:30.473Z" }, + { url = "https://files.pythonhosted.org/packages/25/88/1d7df7201750b37765ef2b23bc1c526c028dadde80afa0f57a118fc01182/propcache-0.5.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:87a3caecf8095e48dc72f84bfa42e23a848cf410cc9cc13031fba4869b706a21", size = 52460, upload-time = "2026-09-16T00:14:31.692Z" }, + { url = "https://files.pythonhosted.org/packages/83/4f/48865bd02a16ee5236bc46166b2946f37b93e07b0eae355dac0be0b216ca/propcache-0.5.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60a64cbccaa11b7760ce705a14ada17ba459e7ca9f23ba587eb013821032d7ef", size = 251768, upload-time = "2026-09-16T00:14:32.908Z" }, + { url = "https://files.pythonhosted.org/packages/b0/19/3742a5eed62317b03b4002ee865dc9fd720308bdd0da1f29a5786c630311/propcache-0.5.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a74bfa37147cc08fb29df10bd9c16f40fa7f860cd3a6d2fff853323a94f6e17f", size = 257723, upload-time = "2026-09-16T00:14:34.267Z" }, + { url = "https://files.pythonhosted.org/packages/cb/d5/ee6350fb0be9122bb6c67082a876d34b90d980d100c106af4b81023e04f4/propcache-0.5.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a4d7a54719b67338a305dca2ce6aafe366817df94ddfd4b5514374356f5ca546", size = 265597, upload-time = "2026-09-16T00:14:35.56Z" }, + { url = "https://files.pythonhosted.org/packages/85/9f/83a07b6ec0e043c050cfdd35fb0cf1b7897b91d554d6eea293740309afe7/propcache-0.5.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2814ecd8e818f487bee4b0f921bc4d1c176cc5fc71ac0f072d0fa67eda4ac14b", size = 250424, upload-time = "2026-09-16T00:14:36.894Z" }, + { url = "https://files.pythonhosted.org/packages/33/2c/a763a8251f50fba042af0fb1f02bfec4b31381e40aff760db2be7b2e1f84/propcache-0.5.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6af4693716bfb03f1752ef1b30faa593db2c01d5272e9b8564a1549452a979ab", size = 216748, upload-time = "2026-09-16T00:14:38.369Z" }, + { url = "https://files.pythonhosted.org/packages/6a/e2/4d11bea8fd6a777149c6c20645f873952eab5de3a2497aa11648ec9ab6ab/propcache-0.5.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4fbc1a15dc8cd1689508758d626b372b1f09d28d9577667feaf9e6bfcd8efcbc", size = 246533, upload-time = "2026-09-16T00:14:39.82Z" }, + { url = "https://files.pythonhosted.org/packages/9f/36/6683597de4907e70c717e3588c541202c66086a72ff3db58be49de66e72c/propcache-0.5.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:cdee8205a44d0be91bbac4c41b95d86641b72dfc7aef1279400e4fda3f26a937", size = 238173, upload-time = "2026-09-16T00:14:41.259Z" }, + { url = "https://files.pythonhosted.org/packages/85/84/cb08d79f1762daafeb2b030c470cd0c725c97b8ad67412457c6f35c53e9d/propcache-0.5.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:9a2a8a50a93dee0268a860a07fa3b4bd968f8ce4dbd794957da772f395368526", size = 251128, upload-time = "2026-09-16T00:14:42.652Z" }, + { url = "https://files.pythonhosted.org/packages/c2/0d/41b848036db6621370c1f2e5471a7da8149c730f8552a5257567721f4576/propcache-0.5.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7ffafcbfc7b549ab940047e505c831eabac5e67de53e1bc174adbc5285c55944", size = 214821, upload-time = "2026-09-16T00:14:44.112Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b7/adfae4bf9c63bccf12e2d9690a175c6579047a6eec3b5a6a5f51428c15e2/propcache-0.5.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:d1f5a500bfcbb2c0ab85e98a0dcd70f5899d34efe365a0187700369a79603031", size = 254793, upload-time = "2026-09-16T00:14:45.429Z" }, + { url = "https://files.pythonhosted.org/packages/51/6f/eeca9647245d5f92e87d53e5f14335bb42fce1a7e6842c8045b364eded8b/propcache-0.5.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8a235f73d6e020855dc29dff012d920c02ee0feab8d73a24185a7569f4be1161", size = 247134, upload-time = "2026-09-16T00:14:46.976Z" }, + { url = "https://files.pythonhosted.org/packages/5d/a9/424e38838793d37160b4379c702f61c74c598fc6cd17204adbe3c554f7a8/propcache-0.5.4-cp312-cp312-win32.whl", hash = "sha256:b3083bfe87f95c756e610bd8025f26cbd1cd4aaa03a422f2d65efb7a97cd53d8", size = 43073, upload-time = "2026-09-16T00:14:48.338Z" }, + { url = "https://files.pythonhosted.org/packages/58/7b/6e8ef26f6d510a7916064fec68d55fcbfbdf7eb01e377480d66a122152d8/propcache-0.5.4-cp312-cp312-win_amd64.whl", hash = "sha256:98914de2c4d7f0f9f4a8c6ea4bf05841f4175796941e3ef7d47eb718f22311fb", size = 46190, upload-time = "2026-09-16T00:14:49.99Z" }, + { url = "https://files.pythonhosted.org/packages/08/b9/72028c5b56ced97f456de6aefa79435ca64d7f77af78ea8cf3c76fc5195f/propcache-0.5.4-cp312-cp312-win_arm64.whl", hash = "sha256:8876b39961e33d912afe3c1bee18ee564fdad0206f873cc15d522756b7f50737", size = 43075, upload-time = "2026-09-16T00:14:51.155Z" }, + { url = "https://files.pythonhosted.org/packages/78/4c/3b1365d58a667689e067e13d055fcd92bdf8d9a2fca3d9201b47ed5b3631/propcache-0.5.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:36c0d9db44b523ef93d03341b1c42d69ff01d673c053d1b1c6c3a363bcaa39ba", size = 85290, upload-time = "2026-09-16T00:14:52.342Z" }, + { url = "https://files.pythonhosted.org/packages/8f/61/5f9c29c3aa67c30238c4eadf95149b1d983a48f69b86b0cff927a7d6df13/propcache-0.5.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1d52a05dc417279f7e5c7618c5dfbbc29923aaf9bc0a5c1802ddcebf54c61a0", size = 50027, upload-time = "2026-09-16T00:14:53.67Z" }, + { url = "https://files.pythonhosted.org/packages/25/7d/c1ab1ef09e9d4d835be5d58c0a32a1e1de8397abaa4e502a9d4141328cad/propcache-0.5.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:44149f46500a0a41b95b4d99c2e586a77319539730607b9892974a092788b111", size = 51425, upload-time = "2026-09-16T00:14:54.826Z" }, + { url = "https://files.pythonhosted.org/packages/73/36/0093091ebb270fcd1bc1f6e095f93b2e0ed7f1011c28837dc2dbe5f96b99/propcache-0.5.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dbab5f5ff6897c81f355d079010cdae85b02e5a0b518b5251523b8ad8ae9ac3c", size = 233595, upload-time = "2026-09-16T00:14:56.09Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8f/0de9d4c8e05ce0be71b436919a216bd7fc5cc6e2691c0602295efb22b9ed/propcache-0.5.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c3e98c55bde2bcf7db3c70d1aed7ae9aa8aebbf19a250c66645cde44cdb8b867", size = 240318, upload-time = "2026-09-16T00:14:57.674Z" }, + { url = "https://files.pythonhosted.org/packages/7d/71/2b35e91455209b85ee98f7859583e0814fab57d3af0f2381aaee34c37304/propcache-0.5.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:db3ae52ccc150dbc84704e9d642743897f3e1c54742ff34cacb661e52e3818a9", size = 246649, upload-time = "2026-09-16T00:14:59.352Z" }, + { url = "https://files.pythonhosted.org/packages/ed/74/08e6c1faf26ee2732023a3828787ba535557122774f4a386b1f715cbd8e0/propcache-0.5.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f85915e00dcb1cd9f2f890ead064ed40a27df06f0db65be427b29482ae357572", size = 234316, upload-time = "2026-09-16T00:15:00.696Z" }, + { url = "https://files.pythonhosted.org/packages/5c/9a/08385733c9321c9bb78039d3ff31045e4fca962d9665023c4eb70f998819/propcache-0.5.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c2ba30a89035b57b73e00475de948521602f543d79ce01db10b04b36c4c76fc8", size = 204666, upload-time = "2026-09-16T00:15:02.019Z" }, + { url = "https://files.pythonhosted.org/packages/1d/f4/e87bc7629af9a14a752b218764a78742d73c2c563ac58315da6841f0cbe4/propcache-0.5.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ae58f361bd5dae942717c65d3413b478c70aea9c462599e7b9adad3731db3894", size = 225900, upload-time = "2026-09-16T00:15:03.394Z" }, + { url = "https://files.pythonhosted.org/packages/d9/6d/11014938d3fe9bea2ea2dcf930f26ed565bfb2f5be3c756362ea48c92636/propcache-0.5.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:96f7c5c15656040ddcbc51e56dc59b58aa25999d743c126abd425b9766ab43e9", size = 219988, upload-time = "2026-09-16T00:15:04.811Z" }, + { url = "https://files.pythonhosted.org/packages/dc/72/fbf17c589f92c0b3bbf6709a425661f8ef2ed0d46b38985a7d7b5a0f6b91/propcache-0.5.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7cc528e760a8af06f2b13e9b9f362cd90c7c718ea61228a96dbd31ba16ed7f47", size = 233611, upload-time = "2026-09-16T00:15:06.498Z" }, + { url = "https://files.pythonhosted.org/packages/55/7e/dbd637572a279692e5518d117274a9331bf5faac59f191d30e82521a3ec7/propcache-0.5.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:425f8cc86ab5018b4b8d4a23bc8e74d964bd3d757c3702e301aa79be76c53f6c", size = 204333, upload-time = "2026-09-16T00:15:07.961Z" }, + { url = "https://files.pythonhosted.org/packages/ba/5a/f99c92068f1e0f5c886899ce0e4a619db376ca98c5279d93f95bd86906af/propcache-0.5.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:a5793c7698a53f56f4a1889a4737c7eeb1b7ad0842fa6b1abca22913ff79c8c1", size = 235177, upload-time = "2026-09-16T00:15:09.334Z" }, + { url = "https://files.pythonhosted.org/packages/ee/28/95456fabd2daf6be89049a13fbf03341756014d2959c83d12957d4c49694/propcache-0.5.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c02c0e570c5c7e077b0181a9f3cdb7d4c3617d1cda6b5c95bd5d34022923d82c", size = 228982, upload-time = "2026-09-16T00:15:10.729Z" }, + { url = "https://files.pythonhosted.org/packages/b1/bb/df90f62c9cf7c93ea235f6f9405143bba802914607317266dd81fc8d737e/propcache-0.5.4-cp313-cp313-win32.whl", hash = "sha256:3e413d7a4a9b4866b7a761d6060d434b64d23cd35122eda3b026a0bbe8196b25", size = 42611, upload-time = "2026-09-16T00:15:12.111Z" }, + { url = "https://files.pythonhosted.org/packages/01/bc/e0a7b84af04ec02d73a48aa71f091e1e4a2107e3074b7ce12195b66901f4/propcache-0.5.4-cp313-cp313-win_amd64.whl", hash = "sha256:0c889f6fa84957bc7e8b4eab71fd16a0455068d5045e3aa40c733071d2b2fd77", size = 45342, upload-time = "2026-09-16T00:15:13.519Z" }, + { url = "https://files.pythonhosted.org/packages/9a/70/50b031cafe72a5c1878b903ee87303f71313345566bf3d6ec202e5ddc9ec/propcache-0.5.4-cp313-cp313-win_arm64.whl", hash = "sha256:69fc35c0779522da366c563e5faf203ffc1f8ff0021d5b1337fa4efa5be73177", size = 42408, upload-time = "2026-09-16T00:15:14.788Z" }, + { url = "https://files.pythonhosted.org/packages/33/c9/07e227b930c8ae513b8ef1aae3793499be097bffcdf7aee4fb8b33db4cd1/propcache-0.5.4-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:e6720ba44ad7e72174314d0e1fb0172494cff5c73a3a8a2159c3d2402ff15565", size = 85933, upload-time = "2026-09-16T00:15:16.073Z" }, + { url = "https://files.pythonhosted.org/packages/e4/e1/6710bb44510c4e4a8e0f004bbaf3cecfd048141309c77bae56d4e5a6ebc1/propcache-0.5.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:4cfe0a92ae30151869e67a4b5f5e105e4e03ad30b3f38e5211b5bf77d0881993", size = 50179, upload-time = "2026-09-16T00:15:17.377Z" }, + { url = "https://files.pythonhosted.org/packages/e2/22/b533b493d7025456f44518b33e53e000021a20fe7c27b88cf3d341df7186/propcache-0.5.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d759d05634f1b038fb625a66662a8c85e5a8fec912da381b5149ddac107482b", size = 51942, upload-time = "2026-09-16T00:15:18.589Z" }, + { url = "https://files.pythonhosted.org/packages/f1/74/70ac8430e28f21e442c7bcb964eb46c4363f6881ade4aa0e978bfd8d503a/propcache-0.5.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:251c63dd46a0659bb875cb254dc4c1e79ee91a847c737cd62373295afc2235dc", size = 232647, upload-time = "2026-09-16T00:15:19.905Z" }, + { url = "https://files.pythonhosted.org/packages/72/95/f222f13b6fe623310be0eb61a673bf26df439ce27e563ca8e422d0818777/propcache-0.5.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7a8d5ff04eb1f85698a78d20c62a14676e7b960dcafde09a388d60ad377d355d", size = 241541, upload-time = "2026-09-16T00:15:21.3Z" }, + { url = "https://files.pythonhosted.org/packages/a2/3e/763e370340db16115c5e63ad46e21ef0770a7f06928b3d3b62d8f8edfca4/propcache-0.5.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7b9100a93b372418d8688f3f2a3e5b45c64d70ca4d6176e121aca1e3bfc1e32f", size = 245332, upload-time = "2026-09-16T00:15:22.802Z" }, + { url = "https://files.pythonhosted.org/packages/96/d3/e97cd6f5de2176bd90ed4076c7a9b5e09d0f0b9687d00a576507988bb62c/propcache-0.5.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cc07876cfb079b6f6f36d21ce75784ad6c2c6b563eeac0ed26c2fa2669b85df9", size = 232757, upload-time = "2026-09-16T00:15:24.374Z" }, + { url = "https://files.pythonhosted.org/packages/f9/4c/6766e5f60bcda26d244333aa71d0a702c1c9b21b251d543c7af5953d1eee/propcache-0.5.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0951315a6b3142ee2167404d707743f0157c110091342b1aa0accac5cf0e4acf", size = 204389, upload-time = "2026-09-16T00:15:25.667Z" }, + { url = "https://files.pythonhosted.org/packages/b8/5e/ec4bb09a70b26ea99d76a8292c3383b960b296de2b347ac9986678f1761c/propcache-0.5.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bee7d3aed13d56f54e681df38c3a23031bc9e3863f687d9d598825c9146acd7d", size = 228217, upload-time = "2026-09-16T00:15:27.11Z" }, + { url = "https://files.pythonhosted.org/packages/e1/7d/b53922ba7d9e5bf797324e63aa05906ec240871899f779628df068743e2d/propcache-0.5.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:4e985382be6d15da8d0c2710a6fa7b9070fc9ecdeefb7f580e88373984ec8be3", size = 216947, upload-time = "2026-09-16T00:15:28.532Z" }, + { url = "https://files.pythonhosted.org/packages/ff/39/b62eee45e5ea4de094a258cbb3b01c1e856ca51ddfd95b43135c5effd1eb/propcache-0.5.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:9e9ab13760aa8b6d0881ae7cb04fd891d8d490cd2554ea8e79bb278399169bcc", size = 233457, upload-time = "2026-09-16T00:15:29.977Z" }, + { url = "https://files.pythonhosted.org/packages/cc/a9/feec61ed296d993db9dd097e0f6723e3f576a647722367547495e4c5b05c/propcache-0.5.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:1b2f3bec4261a94019575481c726c29850f72e27907773c75b1de421e20e9f9d", size = 204131, upload-time = "2026-09-16T00:15:31.74Z" }, + { url = "https://files.pythonhosted.org/packages/92/4d/411ef380cddad28dc001f1c6d75ec72c76cd3817030f68ec1ccfba0ec6c1/propcache-0.5.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:720cf832eb2d0b0dfee129cb3335a26f6ce3cc45ee1187e8f0731758caa16792", size = 234820, upload-time = "2026-09-16T00:15:33.087Z" }, + { url = "https://files.pythonhosted.org/packages/15/37/c988229753629ef1cfd5198337a83e624780ea2b3787efe9e747c05aad2d/propcache-0.5.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9fb0a5be8d9aa213150e8d8148a42aca4984b285bcad1e69587dc4298edd929b", size = 228350, upload-time = "2026-09-16T00:15:34.533Z" }, + { url = "https://files.pythonhosted.org/packages/12/49/5ef1c5cf98591da3c5b952b39e6a298084cc1ce353bc70f85e82397a5036/propcache-0.5.4-cp314-cp314-win32.whl", hash = "sha256:30cc1cebaf9aef49db06357a50398323ae04d70460c0491837d026ab7d6452ea", size = 43578, upload-time = "2026-09-16T00:15:35.957Z" }, + { url = "https://files.pythonhosted.org/packages/1e/9e/a0ac821a2229186af5e2e3c3635a78abb23cfddca57f38513ab5d70420f3/propcache-0.5.4-cp314-cp314-win_amd64.whl", hash = "sha256:0a095db8e15a6020db149ecbed6461939fe74f6acaa3ae8b702a1fe8c38cd983", size = 46304, upload-time = "2026-09-16T00:15:37.655Z" }, + { url = "https://files.pythonhosted.org/packages/a1/19/c8d0d36a9d16cba5dcee67d389c9333b988c8986a653a61c00a451817a46/propcache-0.5.4-cp314-cp314-win_arm64.whl", hash = "sha256:45488d1a5f9ab5bd90aaa1ca20f50fe1922b8ffad71a2009d2adf41355897aac", size = 43440, upload-time = "2026-09-16T00:15:39.091Z" }, + { url = "https://files.pythonhosted.org/packages/2c/e9/42f1da77cacfc184e6ec929557ef653b7961bbf6f1da460b9221273948b3/propcache-0.5.4-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:53eaa697c4d0422ff4cb714d00231b43352064d97b944033b30c1d57cc506ec0", size = 90672, upload-time = "2026-09-16T00:15:40.306Z" }, + { url = "https://files.pythonhosted.org/packages/cf/2f/4b79940908c6ab8c795097c102999d7bc1f7e0b8604dfd1c232f9d99d67a/propcache-0.5.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:886b59c4d28ca97dd23b025fdfc50a0356be934efbbbca89ad26230067f86fe5", size = 52586, upload-time = "2026-09-16T00:15:41.575Z" }, + { url = "https://files.pythonhosted.org/packages/eb/07/02196ae6320c110235bb343f90dbd34be41f8b8964a3ee30db84ec12579e/propcache-0.5.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3fa15757fea1dfcd5b7745cad9f4638929605531bd4018ab2adff7955f1a403d", size = 54335, upload-time = "2026-09-16T00:15:43.027Z" }, + { url = "https://files.pythonhosted.org/packages/6f/44/f48b9a131985659924df5fa5093f68fe72c7ee375329802989ba3126efc6/propcache-0.5.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6f0093ac3e9daada202c2082439d414a625c57184727a46e112a3fb2a81cb788", size = 297567, upload-time = "2026-09-16T00:15:44.373Z" }, + { url = "https://files.pythonhosted.org/packages/04/a1/418d956d2735139f77fc35262179f1f52c23aa666de5a8ab3819c1ae7854/propcache-0.5.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3cd3a7edb6b95b9b33998135ebfa18d709da82290fb8f27c858970b5a12c8b56", size = 297477, upload-time = "2026-09-16T00:15:46.048Z" }, + { url = "https://files.pythonhosted.org/packages/69/fd/ff811fdb6d3d3e67fd9bbfb75881675d34a42d0ef29a45d33e3e233dde07/propcache-0.5.4-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c174bfd1c48a1b51a3078e95586dde718374bac79719ab3541ec9e74aec40574", size = 302669, upload-time = "2026-09-16T00:15:47.458Z" }, + { url = "https://files.pythonhosted.org/packages/fc/57/527910c455b5ec62f6871bef45d4f79fea16cb8c966ba0d4a07f0339ddc4/propcache-0.5.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a219f0ac59817a9114dd2aa57c13180f993e819ba658c7ddab4b66ed1ee0d370", size = 287908, upload-time = "2026-09-16T00:15:48.99Z" }, + { url = "https://files.pythonhosted.org/packages/1d/86/f69ab82707534a0cb2057bdca04f9200a71214c7551800f9d34d6ac39e4f/propcache-0.5.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:17a7400cec0256f0a71ae71f9da398f9894c956ff6668a1c9d317b3367316320", size = 249804, upload-time = "2026-09-16T00:15:50.486Z" }, + { url = "https://files.pythonhosted.org/packages/27/19/60677af50d93be4256213de7cd487f056944c048b9c0b6f2e45b3a30f666/propcache-0.5.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:978f28401afbc76cdc3df9e1717b4229a06b626a1dcc75db4e1f2beb3884c3e9", size = 282344, upload-time = "2026-09-16T00:15:52.029Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f7/a0057808a91fb3b6a5f3602b528f0cdcb3d53e0ff8315d73fabdfdf8fec4/propcache-0.5.4-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:4a1f4f5ffa55dce6307631f3cb2948e117e665966ea512e0d502b16c24f567e7", size = 270167, upload-time = "2026-09-16T00:15:53.466Z" }, + { url = "https://files.pythonhosted.org/packages/83/c8/f4a865490df0dc0c8531d4e59ac411cb6dc24bb255d2396a6f1c60a368f4/propcache-0.5.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:213bb68d9ced5cf2bf717b1071bf2b09b4b04c426256f9fe6d054c60318424c4", size = 286551, upload-time = "2026-09-16T00:15:54.995Z" }, + { url = "https://files.pythonhosted.org/packages/b0/67/b4faebde9da4e8173d0e5a30e8cd31335914af7ef350b988f27fec588cfd/propcache-0.5.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:286867fb156488c251a3721766e380ac4495e4fd6b51aaa1403d89ce7f4359d9", size = 249595, upload-time = "2026-09-16T00:15:56.505Z" }, + { url = "https://files.pythonhosted.org/packages/f6/40/52e1dd5636e9f5a27f6b5a4b4e2f33c322fd72afe956c397d82523ec4a80/propcache-0.5.4-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:445ee3bfb46e85838387fb3c536a73cc0b994dc192b004e40e170adc54aa2a7e", size = 286700, upload-time = "2026-09-16T00:15:57.985Z" }, + { url = "https://files.pythonhosted.org/packages/d5/0e/30b2b324b93ff31a0bab539c102aae59e84e444031b2742150a7646aa1bb/propcache-0.5.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:48cb48c5346a97de792254af77715aa2529c2a1ebc5f586aa0aae44a02f1fe57", size = 280500, upload-time = "2026-09-16T00:15:59.487Z" }, + { url = "https://files.pythonhosted.org/packages/64/36/721bb59f682ff060d0c8df64274fca8cd0521b1a54506c2eedaef795b7f5/propcache-0.5.4-cp314-cp314t-win32.whl", hash = "sha256:03b229037d25b801e7af53fd52b9fc49d9439b036fca1e087e02780631adfa97", size = 46121, upload-time = "2026-09-16T00:16:01.349Z" }, + { url = "https://files.pythonhosted.org/packages/c1/86/0b1b80fa1ac3a0aac44e2922a6964fbe9cd52af5eab8fa933bf9e90b030c/propcache-0.5.4-cp314-cp314t-win_amd64.whl", hash = "sha256:8a1fc236528c457cd739c88abe823da851b7ab645d72792f88658114cc340c12", size = 49154, upload-time = "2026-09-16T00:16:02.901Z" }, + { url = "https://files.pythonhosted.org/packages/69/4f/9fe6f05a47cb550c823155052116f710064b6be5c6e8ec4e9faae7e18115/propcache-0.5.4-cp314-cp314t-win_arm64.whl", hash = "sha256:135036c5cfc93864affb0f9af9a27e5d7a71cb7bd745e7b6dbfc2d56cc30e827", size = 46005, upload-time = "2026-09-16T00:16:04.266Z" }, + { url = "https://files.pythonhosted.org/packages/58/25/895a11d1e4c5c2acc6d816e2bece34e02d9dc92f2182ae276cd819e9e804/propcache-0.5.4-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:45bf2e730ab8905d0527fe05a86500f406e64305c34cc81ebe64b4617cab9760", size = 85634, upload-time = "2026-09-16T00:16:05.599Z" }, + { url = "https://files.pythonhosted.org/packages/58/41/c0acd69271de7a1cf439e77d5d60c18575fd09bad56e798b95fa23458ea4/propcache-0.5.4-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:31eb43ba2edc704ab2ec27815315dd8a19def0fb16215be4cfe8d32fe78ffd51", size = 50084, upload-time = "2026-09-16T00:16:07.384Z" }, + { url = "https://files.pythonhosted.org/packages/a5/1a/ad561f99f90884089e6403b76c220610809429ba868a81a2e7ce115d32e0/propcache-0.5.4-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:174507f82d3594622acb1dd2dafecf2d899d6d506335494e7107767bf05f3aae", size = 51692, upload-time = "2026-09-16T00:16:08.956Z" }, + { url = "https://files.pythonhosted.org/packages/e9/07/057bdd3a9609ffad59b06239cceee784b047f6c720247bfaa36d2103e138/propcache-0.5.4-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:50e337653721d20ead710da33bf44487fbe8a0db8782714b60306481e9f95b51", size = 232947, upload-time = "2026-09-16T00:16:10.466Z" }, + { url = "https://files.pythonhosted.org/packages/fa/dd/d36ad35986718530498a65e45e3713f9f0e6a580f192ef02d2ef7cae9b52/propcache-0.5.4-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0d21d0d2c82bbfeb1677a9711f38df968f9837576102bb4add1bd449d28d88f1", size = 241250, upload-time = "2026-09-16T00:16:12.056Z" }, + { url = "https://files.pythonhosted.org/packages/fb/81/f1459415cdb6c10d46942779de39bb59a77b38e5a76bb1def9227962eb45/propcache-0.5.4-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ccf4f7a79e26bb7efb06ecd50c177833b71df05cbc748701372325e6bcc17f6f", size = 245150, upload-time = "2026-09-16T00:16:13.596Z" }, + { url = "https://files.pythonhosted.org/packages/ce/4e/58b9b1460afc97a4c0b17ee89af701c4011d4d7f46470eba3aaff76a8069/propcache-0.5.4-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23278f808cd81d5ada7184a76606b925fb3389c60e1077b2cd7da7b1fcf0553c", size = 232166, upload-time = "2026-09-16T00:16:15.126Z" }, + { url = "https://files.pythonhosted.org/packages/b9/c6/5a79e0eda3e7b6987d03d8c622ff6d52a42165a12e8418eb37694b9cc4b4/propcache-0.5.4-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e738ab81179510ce79b2eac9a6ecf47feffd9e76d1c72e403005dddb6e36c06c", size = 206085, upload-time = "2026-09-16T00:16:16.713Z" }, + { url = "https://files.pythonhosted.org/packages/4e/72/940aed42c73f9da345ca2de0f6e835c726498159abca5f1ef14fb0a2af8a/propcache-0.5.4-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:a419ee85e654927baabda3929c03c0cc1112bf472ff0dfd6142f4e3a81ca4162", size = 228460, upload-time = "2026-09-16T00:16:18.352Z" }, + { url = "https://files.pythonhosted.org/packages/85/71/3f54e1535c8f323d91ba566044d7c2b39ff6f6a2f1d0bd9071779d07b9b3/propcache-0.5.4-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:b61805357d966680acf68b3b6d49772631ed9df44ebece10ff1460e117a7da8a", size = 218350, upload-time = "2026-09-16T00:16:20.064Z" }, + { url = "https://files.pythonhosted.org/packages/a8/f4/025890cc389ac3ec485ecec607d4a7ca47e15bfa2a465746ab98af602536/propcache-0.5.4-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:58134228927cee6c047d626c08e60a81be604a20578a12ce752cc5c9a84d4826", size = 233156, upload-time = "2026-09-16T00:16:21.624Z" }, + { url = "https://files.pythonhosted.org/packages/04/29/b39cae08c87c140d3d274f0a2c058cb5588e836175c3309e260b230ab07d/propcache-0.5.4-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:350b272b2279f4135a64fc0c304a5d08e28a137c9573442c606152446638a831", size = 206206, upload-time = "2026-09-16T00:16:23.204Z" }, + { url = "https://files.pythonhosted.org/packages/18/61/e16462ef18a87247dc9ebbd5c606f46d5ce67e708bd9cc734dd0d9222564/propcache-0.5.4-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:45bebbe252550fec975ba3b62bc6f931643cfd3b5464ef47619cf3fef154e01c", size = 234469, upload-time = "2026-09-16T00:16:24.841Z" }, + { url = "https://files.pythonhosted.org/packages/9f/84/b6a1490922427204fc47df920ed002eec709621de6b79b11592bf45c623a/propcache-0.5.4-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:ada748108a43d29b7c328ba7db3755327cd94f028bcc1a7ee3f0addcfacd9c38", size = 227311, upload-time = "2026-09-16T00:16:26.549Z" }, + { url = "https://files.pythonhosted.org/packages/ff/5c/5a59527582e9bcb694b2f08b9894134b65a0f5f79dbff174f054f5f74ed0/propcache-0.5.4-cp315-cp315-win32.whl", hash = "sha256:ee19113bce2f3acd46432050688b70f61acd6857d75abb9ec96341b7e9ced123", size = 43512, upload-time = "2026-09-16T00:16:28.313Z" }, + { url = "https://files.pythonhosted.org/packages/26/07/93cf699ed363681e754d7c3fad587fb09ef6b65618ee193332ad16a68d7b/propcache-0.5.4-cp315-cp315-win_amd64.whl", hash = "sha256:ceb3e879afac028f93d272c957814695dc5569e4904262dbee92f6c41bd5e4a3", size = 46264, upload-time = "2026-09-16T00:16:29.751Z" }, + { url = "https://files.pythonhosted.org/packages/65/10/fef04fbdcd44a4a163cb5ff5674599c6d6fdefd64a5a459438f9ad2ba042/propcache-0.5.4-cp315-cp315-win_arm64.whl", hash = "sha256:c83acbce9f2b5e3f5f5eda9e53d2001fed22fcdfef81274a9e02d8fd53b70a30", size = 43395, upload-time = "2026-09-16T00:16:31.5Z" }, + { url = "https://files.pythonhosted.org/packages/70/f6/7e2f4dab0b92ab46111bd48cee9ee1e5f519514c44e3779ede5358d7ada0/propcache-0.5.4-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:a5e8ef588c109725dc713ba69aadcac00a1ef90c2ce9c0a8c7075128f569f47f", size = 89825, upload-time = "2026-09-16T00:16:43.115Z" }, + { url = "https://files.pythonhosted.org/packages/9f/8b/dfeff925cb6ced97ede701d5c6a99998da963c6f2e06abbf879c9dac5b54/propcache-0.5.4-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:4d86476a935c88963d9b8e1a9a0d38188790e9622169bfbafa173046846709d3", size = 52159, upload-time = "2026-09-16T00:16:44.754Z" }, + { url = "https://files.pythonhosted.org/packages/24/6c/924c810be5b7cf218ef47e707cf06d34adb4e3f3a31e3c24c55c6d945a88/propcache-0.5.4-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:f5470694918830da62fac9e69133b53d23b736d7070e587b27a4a2be37e08e68", size = 53956, upload-time = "2026-09-16T00:16:46.762Z" }, + { url = "https://files.pythonhosted.org/packages/3f/b6/9ed0a5c939b58b6bed740a05b5d0f919f0b318d03284b4b6d81a0fe8a29a/propcache-0.5.4-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10ef33a68a61ce317e095fd2e202a592ea92392b90944a78c993f0d9a73ab06c", size = 295235, upload-time = "2026-09-16T00:16:48.577Z" }, + { url = "https://files.pythonhosted.org/packages/5a/eb/5ce886e902a2e781dddf110993d5329458a9b1a8626b876c65e5e25bf413/propcache-0.5.4-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5cacf3c9efd09df409dc33654dd077e1c245ba8fb747b0f0236ef41b7c49b589", size = 294463, upload-time = "2026-09-16T00:16:50.539Z" }, + { url = "https://files.pythonhosted.org/packages/f2/88/c98f49183ecd3e5b204a556f0ca47baa02c2206a500fe8c7ec1726297b0a/propcache-0.5.4-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:770e8209d018175fc0063936fa9583b6d27e88c5ad31543f3383d66080efdd62", size = 300081, upload-time = "2026-09-16T00:16:52.423Z" }, + { url = "https://files.pythonhosted.org/packages/27/0d/c5090f9e6f67cbc30a2b744c7bb0f8006dcba5ec1b0d82f866ae1cc7c5c4/propcache-0.5.4-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03969626faf0783a592dfa17e28eac06018bd0b44dafae6943d53b92421a7f72", size = 285360, upload-time = "2026-09-16T00:16:54.141Z" }, + { url = "https://files.pythonhosted.org/packages/ac/9c/34a55396910583ed07926669ab309dde2213a2dec05a7e946bb90ad66908/propcache-0.5.4-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ef3b928d9c984322b5c44e6964d8dbc653da87d2d8ee1647fa6da43072e650a9", size = 248014, upload-time = "2026-09-16T00:16:56.062Z" }, + { url = "https://files.pythonhosted.org/packages/cd/b5/c0a142b656093ca397039dd3fe166cbb87c945712b534546514a24cd2611/propcache-0.5.4-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:7177c43eddf10a0893c4fec52ebb408fdcd7f7d63962caace9180d8f81b14ece", size = 280662, upload-time = "2026-09-16T00:16:58.044Z" }, + { url = "https://files.pythonhosted.org/packages/54/28/fab2809c2e337fe26becea9648e84d5cef46075c91b826acb13e4f9dd04e/propcache-0.5.4-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:420162a77f94eb1cf5ef7893f500016dabd548e73de956785a1dd899cc73006a", size = 266149, upload-time = "2026-09-16T00:16:59.702Z" }, + { url = "https://files.pythonhosted.org/packages/3a/11/7ddf336288b2678a5f054f8da2e2bd1a719f5d4b7de714d9c6bd588a2313/propcache-0.5.4-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:3eb2e820e8e2101407da93f17c57cbb7d225461955fc60105daaba14cd421ee2", size = 283097, upload-time = "2026-09-16T00:17:01.459Z" }, + { url = "https://files.pythonhosted.org/packages/1a/ae/351b1a5225f5473c411d9a612a229ae147cf0cf65c72ad838b87219ea8e8/propcache-0.5.4-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:13e52b6e0bde97dee98ab66552dbff2931649c96f1ac432eac299fe689ec373b", size = 248160, upload-time = "2026-09-16T00:17:03.298Z" }, + { url = "https://files.pythonhosted.org/packages/3f/d0/7f79f061e30d135bb615c9782c94a74652033d00b49254edbbf35a9165a8/propcache-0.5.4-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:12682126712ddc19b70ff819debbd279e58adf1f0c8f8f8138c18ade2044b284", size = 283036, upload-time = "2026-09-16T00:17:05.238Z" }, + { url = "https://files.pythonhosted.org/packages/53/3c/016f1cad8bf4c428d748cf399b2bac603026fbfd6966e6a5579b5c5b6956/propcache-0.5.4-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:3af0c8642b2da4815d86e631232ac8286e17644fad907c19508aa8e7cb4ba8ad", size = 279350, upload-time = "2026-09-16T00:17:06.881Z" }, + { url = "https://files.pythonhosted.org/packages/ea/60/d8f72cb24b412487ed4c397f539117d3b74c3c33dd32020e91fe00a958a8/propcache-0.5.4-cp315-cp315t-win32.whl", hash = "sha256:1df8d8561b21465c5dd56110a01caf897e026d065b4b84e98a488209094272ec", size = 45874, upload-time = "2026-09-16T00:17:08.567Z" }, + { url = "https://files.pythonhosted.org/packages/d4/ef/8bae0a316d406644450522f2f3d44a4e19632f5f3bb60d1d0e6c53842616/propcache-0.5.4-cp315-cp315t-win_amd64.whl", hash = "sha256:02c0a34f16889cf800f10f0247a564d8ce6eeab6ffcd7c87198f769067eb8432", size = 48574, upload-time = "2026-09-16T00:17:10.077Z" }, + { url = "https://files.pythonhosted.org/packages/57/be/bcc053f66a97355683884b448198e79580fae8e8fa4d96b9bb01614e9913/propcache-0.5.4-cp315-cp315t-win_arm64.whl", hash = "sha256:dc4242ca653c9b30ab51c5f8193323e7bc0928f897ee9103201e59a43abcb72e", size = 45625, upload-time = "2026-09-16T00:17:11.377Z" }, + { url = "https://files.pythonhosted.org/packages/f5/cd/785c64ed382f3f04201870267b02783f63b4678c2acfddc177a3ebcc2727/propcache-0.5.4-py3-none-any.whl", hash = "sha256:62c60aec739ed00124573cce1178138fd690c7676352d67a37328c1cf51d7468", size = 16338, upload-time = "2026-09-16T00:17:13.106Z" }, +] + [[package]] name = "protobuf" version = "6.33.6" @@ -2602,6 +3279,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/28/7e/61c42657f6e4614a4258f1c3b0c5b93adc4d1f8575f5229d1906b483099b/ruff-0.12.12-py3-none-win_arm64.whl", hash = "sha256:2a8199cab4ce4d72d158319b63370abf60991495fb733db96cd923a34c52d093", size = 12256762, upload-time = "2025-09-04T16:50:15.737Z" }, ] +[[package]] +name = "setuptools" +version = "84.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6d/44/f5da03a8ef95d369145c5bb53050e7877c9f3d312e128605fd9504829143/setuptools-84.0.0.tar.gz", hash = "sha256:f4695c21257f0d9b537ec2692c941d02ee143b7cc1276941349a546573b2ef73", size = 1168449, upload-time = "2026-08-08T18:27:58.365Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/9c/c510029fc6ef33a6275cd2c5d3cecd6613dfd6aa401d57c54f1c18852ccf/setuptools-84.0.0-py3-none-any.whl", hash = "sha256:51a52592b3b99e102b609654876bd65f19f999935166d1352678931132b0c670", size = 818216, upload-time = "2026-08-08T18:27:56.719Z" }, +] + [[package]] name = "shellingham" version = "1.5.4" @@ -2779,6 +3465,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/93/72/6b3e70d32e89a5cbb6a4513726c1ae8762165b027af569289e19ec08edd8/typer-0.17.4-py3-none-any.whl", hash = "sha256:015534a6edaa450e7007eba705d5c18c3349dcea50a6ad79a5ed530967575824", size = 46643, upload-time = "2025-09-05T18:14:39.166Z" }, ] +[[package]] +name = "types-protobuf" +version = "7.35.1.20260906" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e1/6c/e3e5b3e10bc328126a39637c138f9ebfd734bf14342b9f3540039b4ab995/types_protobuf-7.35.1.20260906.tar.gz", hash = "sha256:efd1a3862d4c967dad5512ef8d56b1530ac84f182c41735b94004756518c4998", size = 69895, upload-time = "2026-09-06T06:35:28.308Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/4e/f63e826c68f77ef875506d72f225918800346545ee99847bc28f3394f18d/types_protobuf-7.35.1.20260906-py3-none-any.whl", hash = "sha256:5155e48569e0dabff303fdf578db96cd31ea9a4a63b18018a4ceac6b0ae17462", size = 86419, upload-time = "2026-09-06T06:35:27.247Z" }, +] + [[package]] name = "typing-extensions" version = "4.15.0" @@ -2924,6 +3619,156 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1f/f6/a933bd70f98e9cf3e08167fc5cd7aaaca49147e48411c0bd5ae701bb2194/wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22", size = 23591, upload-time = "2025-08-12T05:53:20.674Z" }, ] +[[package]] +name = "yarl" +version = "1.25.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "multidict" }, + { name = "propcache" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/75/16/e8be8e2fb175bbf41a0680381a319f1199fae256588241a2ac8677eafb49/yarl-1.25.1.tar.gz", hash = "sha256:03dd38de09bc213e9a8b29761eec33ee1d5318dac0e49d8af36e4d27830e23a7", size = 246245, upload-time = "2026-09-15T19:35:02.264Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/92/55fa9ee84cb8ec9930a910b5753936a926f918d8cc8965bdac571479c095/yarl-1.25.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:142c06c4d6a35ee3ec5da08499805e879cb3ca7c1fbfbecb0140fe72403818d6", size = 144695, upload-time = "2026-09-15T19:29:53.114Z" }, + { url = "https://files.pythonhosted.org/packages/16/b4/9edc8e605b16b2eb5bd0c2ccc73e2b15aab583862460767fb43f91f11839/yarl-1.25.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:24ce942011a61953e7d313438038f4d32ff21387b775f58a957f7a07dd55ef95", size = 104332, upload-time = "2026-09-15T19:29:55.443Z" }, + { url = "https://files.pythonhosted.org/packages/ae/de/4204e6646e278b1cd4a9cf73ebe8b7cfcf16693f1249b324eb273d67602a/yarl-1.25.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e23c82b63cd7652fc24d33ed6cc17099d607aa3b4fc4ddc75e95062f3d82df4", size = 104449, upload-time = "2026-09-15T19:29:57.239Z" }, + { url = "https://files.pythonhosted.org/packages/1d/fc/21d74789198ad68a23a209ab0d73fe5a52d39ebbd9945041e72827acdd77/yarl-1.25.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8ee202350cf57abf0e9502a41601841019c25d3db7ff52d980aaf31446254059", size = 116828, upload-time = "2026-09-15T19:29:59.031Z" }, + { url = "https://files.pythonhosted.org/packages/8f/31/90891ddb61848c067ebd1fb505ab5902a6e7d4707c2f3823f67ff37883d3/yarl-1.25.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5df89f769cc8ff94c3d7e7603386fba309d25ce5240132d26c15baa8d0e96c4c", size = 107150, upload-time = "2026-09-15T19:30:00.868Z" }, + { url = "https://files.pythonhosted.org/packages/a1/15/f3d18743d3688b5f01aa83034b6c00665692ab20e1d0c89a29000d4979ea/yarl-1.25.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:83e9f4a25085bd4b7214701a0794ff1f50fc633ffb8bdfebf07abdd81c2db126", size = 124704, upload-time = "2026-09-15T19:30:03.128Z" }, + { url = "https://files.pythonhosted.org/packages/99/37/718789d8004775d6a2db86b2afc72b1d156e0590b88a1b8634ddd4bdb8ee/yarl-1.25.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e636b64d24fd9c38053c5e389a1174c66361fa49dcfd220f4dd35b4abde7cb89", size = 129246, upload-time = "2026-09-15T19:30:05.014Z" }, + { url = "https://files.pythonhosted.org/packages/28/b9/7818ac6dec7fbcd16be2d19495807c01e2c38834e57d41c63356a75e786b/yarl-1.25.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e5637ca8d0bd7fb72648a6c7934af4baaccb697657f7438c9d264fc2abb8b0b1", size = 118071, upload-time = "2026-09-15T19:30:06.977Z" }, + { url = "https://files.pythonhosted.org/packages/b2/e3/8d098cafb30a64df7283b5a5dd0d32d22a71b18a1b9ec071d0776ffe5c2a/yarl-1.25.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:683e362b8ba453080f7489c66f4ea794e751c35b72e7eab3575ef784c2fbc7fb", size = 116180, upload-time = "2026-09-15T19:30:08.836Z" }, + { url = "https://files.pythonhosted.org/packages/20/9c/fbe9432478d87e00eef80242e15179639df162cb3d5d8d978fc56dcb6c51/yarl-1.25.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:df23df54b5114a17c2d0ef192433e2e5a9f0c5178c32375e90b7cfc965f349d0", size = 116578, upload-time = "2026-09-15T19:30:10.782Z" }, + { url = "https://files.pythonhosted.org/packages/b1/8e/b3dfd03732236b86b0bcf06a72a37de412ff6f82e719a947c3c651d4dece/yarl-1.25.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:f53dcd26694f148f738edc052b5a69234833e739f10f4c3287bdfd8ec0f7b326", size = 108886, upload-time = "2026-09-15T19:30:12.951Z" }, + { url = "https://files.pythonhosted.org/packages/83/e1/94dacb650d4963d5f844bc3df7ba70288cb7b68ed5a4d070da71acb19b14/yarl-1.25.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:b8075fe90bc08e40b8b8a1874fab42ee4c7b56af05c5886e9cc841397f916908", size = 124098, upload-time = "2026-09-15T19:30:14.963Z" }, + { url = "https://files.pythonhosted.org/packages/d6/3f/c93f76a218258c7bfc60dd27fd56e9102bdbc625517f4a6ee673295fe4e4/yarl-1.25.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:a8c2b841478068440d8b733005d13a5ef535b9928cbc05f17182d410f32ba449", size = 115768, upload-time = "2026-09-15T19:30:16.736Z" }, + { url = "https://files.pythonhosted.org/packages/a8/79/4d93f13c3b05cda3c962805dec28cbc255c50239b3457808abc5633a00c2/yarl-1.25.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:ca32926d7d77bcc8838425c4c95e040a3ace1cb7dfdae599013458dcda2607ca", size = 122461, upload-time = "2026-09-15T19:30:18.901Z" }, + { url = "https://files.pythonhosted.org/packages/c5/8e/e2ba83b3a9bfc1d3b882ad35fa8abc04a7af4d9356f2e329a23d0c8d889d/yarl-1.25.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:192a866877a49993949ef1975864ad8728bea28ee810f6abe1a0729c2b500426", size = 118295, upload-time = "2026-09-15T19:30:21.07Z" }, + { url = "https://files.pythonhosted.org/packages/42/67/cb5ea1baa0c0ac60bda44ce59f601133154af2a2e67722d3517f8793855d/yarl-1.25.1-cp310-cp310-win_amd64.whl", hash = "sha256:3f4d48a6112712973e676bd792121fee470e432d749177162d9949d5c9460a1b", size = 102929, upload-time = "2026-09-15T19:30:23.122Z" }, + { url = "https://files.pythonhosted.org/packages/2f/85/9a1e98de10fc0e738d517efd2a9984c3e7db6cf35fa72b2b373988a9e9b7/yarl-1.25.1-cp310-cp310-win_arm64.whl", hash = "sha256:48796ea00a303961507dc6c8437c4b325a6fc3f95f7c36c71b91ea9a8150963c", size = 98854, upload-time = "2026-09-15T19:30:25.102Z" }, + { url = "https://files.pythonhosted.org/packages/83/b3/2cea721d495ca57f8f414aa4867ee263f486b274e07463158cf52f02ba7f/yarl-1.25.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9d693bf4bf534e9ba3ae2780cfd577f5135629f7b5ac653490859d0b77864865", size = 143794, upload-time = "2026-09-15T19:30:26.946Z" }, + { url = "https://files.pythonhosted.org/packages/55/e6/cd145cff8e5cf60b8b3c41fbecfa2a45028a8dec3fbc52bec03595ff3d3b/yarl-1.25.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ab2054c5531af2a9ba7b69b8ec91e4f884420e83a8c5e579b013084cb57e5e5d", size = 103993, upload-time = "2026-09-15T19:30:28.661Z" }, + { url = "https://files.pythonhosted.org/packages/ce/7c/fbb40fe2d53747c40aa36a9e2bd2178a202f942bda0b670f3306d4aefbce/yarl-1.25.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:564fdc7085d2245ab84f88882fdb1d6ac0723124bff6ded35bfb1c00f812630d", size = 104010, upload-time = "2026-09-15T19:30:31.069Z" }, + { url = "https://files.pythonhosted.org/packages/f0/0b/5a516f70641092283f57cf3670bdb75e7327bcc0dcb5038697e4dfbfd569/yarl-1.25.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:acae6b45d1ace09b6ba3876da43b88366ef368f73b988c7f57e14231753d4420", size = 116444, upload-time = "2026-09-15T19:30:32.894Z" }, + { url = "https://files.pythonhosted.org/packages/b6/8a/d1a627f827b0a404ae0f5647cab0534959081cde13b0756a783469fb3b5e/yarl-1.25.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1fb2a01ba8cd9c5d2c5dc1ec35e0fc951d04b4f037541d4ac090c993ce58b3d7", size = 107565, upload-time = "2026-09-15T19:30:35.188Z" }, + { url = "https://files.pythonhosted.org/packages/aa/cf/c8e0aaec886840a6c4480fe44eaec7cd4319f79a563b79321473be79c56f/yarl-1.25.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e92b6bcc741b86d67606c40d3cb9c7cc8e6c737f81e31f4a94efc204456c92e3", size = 125006, upload-time = "2026-09-15T19:30:37.1Z" }, + { url = "https://files.pythonhosted.org/packages/50/26/0cce366d54a93cdc8342965dc7663385e4db161625cc1e8b18e786753d24/yarl-1.25.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:72c34ac7ad4314c19362d5ce27626dcc8429bd30bbf8c179f4234078851f9492", size = 128717, upload-time = "2026-09-15T19:30:38.904Z" }, + { url = "https://files.pythonhosted.org/packages/95/0c/a71501bbc1a674ff72c4d6c2b75f4d9a5af819f5244c3a7558080a8802c5/yarl-1.25.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5add7b4ca7afeea91d52e4d4e4db3b1fe9885b71f07054560d8c4296b7441a2", size = 117728, upload-time = "2026-09-15T19:30:40.809Z" }, + { url = "https://files.pythonhosted.org/packages/e7/6f/c3267ca01defeed9ed9c4ff9b17bd54915432c405945233265b707475d1c/yarl-1.25.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:def538065f9e4d4cf1ae164bd59aba00dfa84f03923e0de4c3788f252d6bcd17", size = 116224, upload-time = "2026-09-15T19:30:42.818Z" }, + { url = "https://files.pythonhosted.org/packages/8f/69/fad57ee52d648431718ee0f1f68966a99c1352c3924688ffbdcc9d3fe51a/yarl-1.25.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a191bfdb30a79b98e5d175d75285f9fcb78bf0e46ba5efda042e1c72071a0de", size = 116299, upload-time = "2026-09-15T19:30:44.966Z" }, + { url = "https://files.pythonhosted.org/packages/2a/d4/6a8c1e29f33338687ca278cba0a8fbf6525a322c2c02a9a500ccbe041152/yarl-1.25.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:71f42c5b9a948c113bbdebfa544598321431d064ff959d32e99b1feb61d68345", size = 108625, upload-time = "2026-09-15T19:30:46.874Z" }, + { url = "https://files.pythonhosted.org/packages/e8/d2/3a35ae791c9cb6522c106923ff25c3230d999091e5e65511bca23bbd9914/yarl-1.25.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:72849d892954be4d09e569b8b831ac39ce58417fedc767d4308a0fe542018a40", size = 124515, upload-time = "2026-09-15T19:30:49.082Z" }, + { url = "https://files.pythonhosted.org/packages/be/fd/2b022109a6b4af0f7dc371cf7500af380b0d4f034010243e1b0ce218dc93/yarl-1.25.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:efb01a106f971cb3752856bca2318bbdf7f01bd8823779c461586cbe5ffd5258", size = 115711, upload-time = "2026-09-15T19:30:51.208Z" }, + { url = "https://files.pythonhosted.org/packages/18/59/f7586271136c3ddb0126bbfe661844699369b76b555fe38c4efe86870b2e/yarl-1.25.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:a1daf47cd95a7c3a63456336bc5aaa8c86dd3a47d07ed3d0e76132ae4666a5a1", size = 122751, upload-time = "2026-09-15T19:30:53.535Z" }, + { url = "https://files.pythonhosted.org/packages/a0/0b/07f7a2d881f7e16c385b47fc1753382600847cad305dcc7fa0c25828acf8/yarl-1.25.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9489e6abf47ba37f332075a91444c7cfedb03e6ce99fbb2f116bfe1ce810da3b", size = 117983, upload-time = "2026-09-15T19:30:55.277Z" }, + { url = "https://files.pythonhosted.org/packages/aa/9d/8cdceec66a9b940700cb45931741403f045b162afd79bcb93c41cadd0972/yarl-1.25.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7306dee25b8a0e737363f347362b875094b4dc4e367311470656ae420fdbf8e", size = 102894, upload-time = "2026-09-15T19:30:57.606Z" }, + { url = "https://files.pythonhosted.org/packages/17/f1/7ec357db1d3ad2863542d71e8fe64a126bbec17b134cd7d30951196809a5/yarl-1.25.1-cp311-cp311-win_arm64.whl", hash = "sha256:abb1384477f5901d436b5d2e5465954de46ea6098f59163d243660b5c4461d35", size = 98609, upload-time = "2026-09-15T19:30:59.705Z" }, + { url = "https://files.pythonhosted.org/packages/75/b3/cd32ac66ae622b854c2df0ac52106dda220d361b65a64fde7d5b3684aa3f/yarl-1.25.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:94d7aa6debf92a1dd14cb5280b083a764169a13cfb23a452111160274ed989f4", size = 144798, upload-time = "2026-09-15T19:31:01.821Z" }, + { url = "https://files.pythonhosted.org/packages/61/fb/a2c52a8007c2051ba74662afb112ecf3d00346af4c25e33df9d80fd14fb8/yarl-1.25.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:83d4a37e4b95da4d8bda930d6d35b75b4cdadbacbb4980cae290ea3100b5d51d", size = 104583, upload-time = "2026-09-15T19:31:04.05Z" }, + { url = "https://files.pythonhosted.org/packages/be/dd/ee38aec8e09fdf957e50d4085453fbe202f56c6c3b4cf07b81cdb4f09ee9/yarl-1.25.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e029648f9c951db30e98a7d7ec90835db88ec4b32820efe2a9bdc2287e032eb6", size = 104325, upload-time = "2026-09-15T19:31:06.338Z" }, + { url = "https://files.pythonhosted.org/packages/1e/b3/058dbfb1857b484c9cf9cc135659f50b85ce66e03c99e44dc2f7b6161f55/yarl-1.25.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4d781294bb815ecb5ea57ff6bbf8038e0a31a95fdf3e1788f66e0dc100d64b58", size = 115358, upload-time = "2026-09-15T19:31:08.593Z" }, + { url = "https://files.pythonhosted.org/packages/db/39/29693446cf0cf6b15a0e2f75a5d40f93c56819b05b0622196f45e95b5cc0/yarl-1.25.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e12c538e00e7c1b286a07061046b90e8124e6a9793efae2c70db6a4aad07faad", size = 107658, upload-time = "2026-09-15T19:31:10.802Z" }, + { url = "https://files.pythonhosted.org/packages/86/b3/3c4dd7e1af43b931fba95e0a722737f2ea94a6d199c802585282831d7abd/yarl-1.25.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7e4de3ac4adbad3d0bc7c6f4360a7dbff5de2f15e3b723be3198074e17fd9c40", size = 122660, upload-time = "2026-09-15T19:31:12.84Z" }, + { url = "https://files.pythonhosted.org/packages/bd/b5/1b60dbc3cfc9c5712b15148c206748f2bc93953ffdbe25ea75b63dfc89c9/yarl-1.25.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:419f392a1da624877975709e3864dfe833af6cc7671b39318086d456e288380c", size = 126506, upload-time = "2026-09-15T19:31:15.088Z" }, + { url = "https://files.pythonhosted.org/packages/bc/7b/ca212cbe170ac8b96e45317ecbcf9c3c3ecf0cdec98d5b088a9c4088929b/yarl-1.25.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c6f117789d22dce188e5754e8bc65b7e6ebf8cb73963b9fa761f672a5883769d", size = 117050, upload-time = "2026-09-15T19:31:17.241Z" }, + { url = "https://files.pythonhosted.org/packages/cb/c3/72b4938cdbe619ad71ac156182faef4908846b84dc3ca4dbb4c4e6f84014/yarl-1.25.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:80e47012e730da131c9f059c80936783f9659aae22dc31c03c0595590d11ed54", size = 114174, upload-time = "2026-09-15T19:31:19.294Z" }, + { url = "https://files.pythonhosted.org/packages/e8/43/268717870f9ba0cc9701a95181587f6dc8c5f387aab4aeecc83158f38a79/yarl-1.25.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e80f557716fd765439577131e526b8942ffc2c07bdbc5e39fa62f660ba1e963f", size = 114944, upload-time = "2026-09-15T19:31:21.414Z" }, + { url = "https://files.pythonhosted.org/packages/da/84/baa5bf504d51fe062c4bcaf62936da97fffb43285978d0b39984824231fd/yarl-1.25.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:f61964f235a43738bfac50da46fc4254943a7eea3051aeb0b6fc7c992c29fadc", size = 108263, upload-time = "2026-09-15T19:31:23.388Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/779a2ed9e0152a601a27039bed9aead3f0b79797a67e2c44bfa444622dd8/yarl-1.25.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e546fe1d4a93ebc2910f0d768baff19faa09843ab3f2036a67ed6e69fae4419d", size = 122184, upload-time = "2026-09-15T19:31:25.343Z" }, + { url = "https://files.pythonhosted.org/packages/f8/1f/118e9e5b8f07694d63fd3222e801d7782270003f1a222aa798df3f8d5933/yarl-1.25.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:cce0727fd5ac04d372fa9bbfde9febc2bcf209aadfcf0468e45dec72719895d1", size = 114001, upload-time = "2026-09-15T19:31:27.465Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ae/a4cf1cf372313734b17996d4007f9f73596e7a178b9485802e5494ecf484/yarl-1.25.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af4ea5b37403ef4e30f3927eaed540db942bde01d8d3ff083527c0704d1c9c68", size = 120565, upload-time = "2026-09-15T19:31:29.47Z" }, + { url = "https://files.pythonhosted.org/packages/05/79/ad94f93ca731bc9e44d321833ab96b82a4f9f5f63cf773f81a4aeea5ecc1/yarl-1.25.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:68782fdb4027b8d1eee25ec35e9a6db05e863b899eb0310b3a33b6c3fef55707", size = 117060, upload-time = "2026-09-15T19:31:31.367Z" }, + { url = "https://files.pythonhosted.org/packages/bb/cc/51a7b4abf4ac593b8e7eb3794b28e5a35ae26eed8bc04787628d215af82f/yarl-1.25.1-cp312-cp312-win_amd64.whl", hash = "sha256:7d575b54cb3863ef9bc290ea4b009999d55dc237326131e4853cf33e888fee03", size = 102593, upload-time = "2026-09-15T19:31:33.329Z" }, + { url = "https://files.pythonhosted.org/packages/9d/21/0941a6b93a58b59a1ec75e5333bf06929b671309c43c0cd201c172d9c39f/yarl-1.25.1-cp312-cp312-win_arm64.whl", hash = "sha256:bc3ac7bf569f6b64dad04dd7808c7872dae8a97df657856eac05e9b7e3614a85", size = 97697, upload-time = "2026-09-15T19:31:35.855Z" }, + { url = "https://files.pythonhosted.org/packages/7b/ed/2f3129bbcc9a5c8ba12cc2b29d8060a3bab9c8043c456cfd4b5ca3188890/yarl-1.25.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:25868beca8b6765f8f7d0e11fe6dd7c66dd4b0793b9500286d20cc92352126a5", size = 143623, upload-time = "2026-09-15T19:31:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/17/e1/f1bc3390fdca352826676b531d0712736f156919090206700421d46b2c37/yarl-1.25.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:10b2fd95332f0d716d5eee3c9fb2ce8eada19082de7fee83d32e37992fd75c26", size = 104011, upload-time = "2026-09-15T19:31:40.25Z" }, + { url = "https://files.pythonhosted.org/packages/a8/aa/50acc5c3e5da04172ae3c281c75405af4d2ca911e16120ab0563f4dffb66/yarl-1.25.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0f12afda4eea8c8994a76d4df1875c765194f5fbe8a9d197929ea303caee29ec", size = 103677, upload-time = "2026-09-15T19:31:42.46Z" }, + { url = "https://files.pythonhosted.org/packages/30/d2/7d1e0ab9f8390e1fbcede5a6dbf70d23c96ad09b8c5567f3a514d1ddb0e2/yarl-1.25.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:14b79a30a93a3ce2e8832603fd0ab780ada281b0ba5110b519a634f2d7d7d1fc", size = 115392, upload-time = "2026-09-15T19:31:44.371Z" }, + { url = "https://files.pythonhosted.org/packages/71/e1/5ba1e3a2a22139213655e760919038e8ed7e2d4a99826d0bbddb3beb96e5/yarl-1.25.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4bd6340d20ae2c7ca719b87b426e808e90743b676d05d4c26c4fb5ca71f41184", size = 107493, upload-time = "2026-09-15T19:31:46.273Z" }, + { url = "https://files.pythonhosted.org/packages/f5/53/780653d5e0f73831f467cf13548912e5eec97f21dc49fc8daf21da027df4/yarl-1.25.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:126a2533570c554719ca40a1288fdee1700b6bc82e7131aa69fa85252d92e651", size = 122537, upload-time = "2026-09-15T19:31:48.654Z" }, + { url = "https://files.pythonhosted.org/packages/03/92/d54fa70236c6036271c9c9c09fd978df5cbe3ef49ef6c46e9b833476d215/yarl-1.25.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a3faadac7d812ddac258feb57b9846b60c1b437c4f4b9ad42595c6f6fe4390df", size = 126170, upload-time = "2026-09-15T19:31:50.872Z" }, + { url = "https://files.pythonhosted.org/packages/0e/b7/a82a49bf88340b837ef6972b508a1604ae377b9e6904b46b10cf5f1cf925/yarl-1.25.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:be80550d9bfe83d9b62398a37081a90434e6df2d978ec345c3d2820de6beddab", size = 117012, upload-time = "2026-09-15T19:31:53.189Z" }, + { url = "https://files.pythonhosted.org/packages/ef/78/5d684b411e3f3602464ee9b538db48205038f8605872985f61efb809ced0/yarl-1.25.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e07595c7d6f4db270ceede356a1bd1c07a34f1c26f958d1ed0cd7b48e0d2bba3", size = 114950, upload-time = "2026-09-15T19:31:55.694Z" }, + { url = "https://files.pythonhosted.org/packages/2f/11/51d82b852c64f7fad0fc7a7ff3031517204887e874c722bbca839c0b23ac/yarl-1.25.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:eb96ed1ae6c7d072d60840c0434aef07a2df611812810807fbc54263a6053e9a", size = 115428, upload-time = "2026-09-15T19:31:57.966Z" }, + { url = "https://files.pythonhosted.org/packages/e4/49/9d1978049bf646b9ea918313926453c6901b71c92f097467777d47d36a88/yarl-1.25.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:3feb99222553a8cbedfa52c2f59dd84c3f50d5b582c728d522caf8d72769a54b", size = 108428, upload-time = "2026-09-15T19:32:00.048Z" }, + { url = "https://files.pythonhosted.org/packages/43/35/7b8f1ebb45d7ec3dda7d1909bf44f458de41ef91e2937f107733582a5166/yarl-1.25.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:a2ed0ba415ccdf08f14bf544cb78346d0f76086707ffee24921a2c84dbf1305a", size = 121961, upload-time = "2026-09-15T19:32:02.436Z" }, + { url = "https://files.pythonhosted.org/packages/63/d6/d8b689ab7ca26edeb85f6ff28812aac7a25376eefc1780e303a7bfbaceff/yarl-1.25.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2b49375d22299b0a834c2bca72f39aaecc270d96fb24c30424899676f487b22a", size = 114961, upload-time = "2026-09-15T19:32:04.456Z" }, + { url = "https://files.pythonhosted.org/packages/cf/d5/1a1798ea4dc6b7ee3260010a27907ebc697c95dae99817d817ed446d24aa/yarl-1.25.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ef74070ac553c59eb4f04258722066d6c6135b7baa03b2e9f2da65c096e96d98", size = 120036, upload-time = "2026-09-15T19:32:06.5Z" }, + { url = "https://files.pythonhosted.org/packages/91/8d/b1b35ed7903da6669b1d367cb2c09436acd4ff508029b4f39a0c0c2058fc/yarl-1.25.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0a66db89ea473abeac4b70523cafd94db3772380e565f9d28af7a179b7af71fa", size = 117276, upload-time = "2026-09-15T19:32:09.401Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8f/4db01cef62caff0d7a4593ed694fb8a41a27a11158cab80d290221f13e57/yarl-1.25.1-cp313-cp313-win_amd64.whl", hash = "sha256:1f51020b2eb8a003c84925638ec63c21a750a4bddd3a22ec8eac6a742dadf1b9", size = 101945, upload-time = "2026-09-15T19:32:11.545Z" }, + { url = "https://files.pythonhosted.org/packages/c0/5e/3ce00497c5c0babb74d4130c10c3828ccd215b4819d12020c42429f991ac/yarl-1.25.1-cp313-cp313-win_arm64.whl", hash = "sha256:b10dd0557ba422715b5206b3743192135a6022acca8baec51aa127d0a75db8fe", size = 97270, upload-time = "2026-09-15T19:32:14.127Z" }, + { url = "https://files.pythonhosted.org/packages/80/cf/54023edfab7aa773b860503db0c56e962ccab0922803ee97988c176ea090/yarl-1.25.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:a9ca696eb02e5c02a8afd872ada510eba9b7fe6e68b9572c2e9a9b1941e31e2e", size = 143975, upload-time = "2026-09-15T19:32:16.416Z" }, + { url = "https://files.pythonhosted.org/packages/d7/a8/e6c1be0e6761d0f2d10bbf33a3e1e02b99dc83874d92945d7b461a72481e/yarl-1.25.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a5877f2255aab518ebe528289037699201d5dc5f045f2396cb30aa02db22f57f", size = 104018, upload-time = "2026-09-15T19:32:18.364Z" }, + { url = "https://files.pythonhosted.org/packages/6e/bb/dda344765ffd3430afe1a1c66c866a57fae67786537d4f14607df6505ac1/yarl-1.25.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7a5c3115595995779ee21f2567035793911c3802a43c74f3fbb0314929ec67ac", size = 104156, upload-time = "2026-09-15T19:32:20.459Z" }, + { url = "https://files.pythonhosted.org/packages/e5/5f/ed1538bcd06009fe990d6d283dd7667f639e62a81e35c6d8c6ef6c08fb3c/yarl-1.25.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77e5099b99b37f3cf79c246998ca9f7313a78054cd1809ec46bc1afad47e1c4c", size = 116025, upload-time = "2026-09-15T19:32:22.766Z" }, + { url = "https://files.pythonhosted.org/packages/a2/af/2185daf56b99830d3356ecfada46faaa49945de6626e842b7728088d4980/yarl-1.25.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6efaf45df6a849cef613a03a94c845647456662f85438c886bb67a9c027c8c2c", size = 106985, upload-time = "2026-09-15T19:32:24.749Z" }, + { url = "https://files.pythonhosted.org/packages/c1/65/bc1ae564fb4b04a30b6a8f250e787772581c57e4c3d5cf07ac3359de3103/yarl-1.25.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d5f90e44653c4e0f78501ed9bb7d3fce835a8d62b7c6ed0cb16557534087e743", size = 123030, upload-time = "2026-09-15T19:32:27.084Z" }, + { url = "https://files.pythonhosted.org/packages/6a/3e/e2afcde10d74e53b3fa889960991efb3019beda2b1682a01de720a302056/yarl-1.25.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:632da579b2d879f6bad20f2cfa35ded1efe2f4f77f8abb26a6234a5b236acd2f", size = 126765, upload-time = "2026-09-15T19:32:29.332Z" }, + { url = "https://files.pythonhosted.org/packages/a2/be/415b00c0fe5a0615b062a456b26623d7ec91c2bee20faea1a14045aa0469/yarl-1.25.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:30eec96e8a91bd588ce897c9543f6d5d8d34b28fbcba28a4dedf20ebeae9fe57", size = 117199, upload-time = "2026-09-15T19:32:31.49Z" }, + { url = "https://files.pythonhosted.org/packages/97/27/3d8c63ddd3e8bcfd033748ab93876678ce59bacd66e4cb1ed851c9c5b37e/yarl-1.25.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:12b6bc4906e11f5e1a1cdcb12296e7afbd366c783cc8073403cd2fb74334e453", size = 115187, upload-time = "2026-09-15T19:32:34.137Z" }, + { url = "https://files.pythonhosted.org/packages/39/b7/7a81d0be1a502a26a0d4326c6f2ecb736c824f570ea1c6529f2b0b227b50/yarl-1.25.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9d6ed3d17bccce4c05343e1ca8da13bc5c02c812a4e7282ddd05e8769322d3fc", size = 116085, upload-time = "2026-09-15T19:32:36.438Z" }, + { url = "https://files.pythonhosted.org/packages/f0/69/39fff459916aa0fab42215dc47b759586fd80f94aa56dfc4a7c15ba6e0dc/yarl-1.25.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:f38a70074041d3b7e138e452799f5174198bae5bd5ab2000917badf403908c5f", size = 107996, upload-time = "2026-09-15T19:32:38.959Z" }, + { url = "https://files.pythonhosted.org/packages/c0/39/80b9a55a3335590451d9ecf3eb593a8c635351f4c905ef056d7e8a8fd9e7/yarl-1.25.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:4ca89e4e21854ed27ec753297dde84b16c9f8e53b14a4866fb44457d643c19f8", size = 122549, upload-time = "2026-09-15T19:32:41.151Z" }, + { url = "https://files.pythonhosted.org/packages/42/7d/a179c6757818bb59372a4adafd09f7f26a3b4a0f04c3ae404b544c0b0c82/yarl-1.25.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:1ab7618921a93767387a4b83776f751588f5b5ae9bb5bc96620e2e2e00bca868", size = 115107, upload-time = "2026-09-15T19:32:43.072Z" }, + { url = "https://files.pythonhosted.org/packages/32/2b/a773ac867e4ab53a98ed98e5cefe3bae31e6f550252ca9d1de266f1a40c5/yarl-1.25.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:0ae12ff2b805fa02c4dab838005caef735e39986322698c48588d3beacb65c62", size = 120666, upload-time = "2026-09-15T19:32:45.061Z" }, + { url = "https://files.pythonhosted.org/packages/bc/41/52be6505e85b0f76b4f85b01b5de7e06a0512201abc2c95e14e099549174/yarl-1.25.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:90c30ed53546da833c700115c0064c22120d1b1560f474699fd31f22dd668233", size = 117505, upload-time = "2026-09-15T19:32:47.177Z" }, + { url = "https://files.pythonhosted.org/packages/f5/01/349c0386caedbbe488d519f252df54efac8a1459282d466c474bdd84a620/yarl-1.25.1-cp314-cp314-win_amd64.whl", hash = "sha256:acfa7e22aa6c6e7a5996a41d275bfa01efa7ea56ab890590280e9063e2cf5c1b", size = 103446, upload-time = "2026-09-15T19:32:49.615Z" }, + { url = "https://files.pythonhosted.org/packages/5c/f0/8ec63180f77912f0dc4e5a42760cb8c08d20da1d5ace3578a01b84d1f3d8/yarl-1.25.1-cp314-cp314-win_arm64.whl", hash = "sha256:8e7d98cdbb6d71e726f7d525952867096053d1f290dd4e3c50d7d313a136f414", size = 99159, upload-time = "2026-09-15T19:32:51.686Z" }, + { url = "https://files.pythonhosted.org/packages/47/7d/92d2220d6886b70ab1ed8579533ac2af2dfac716d5d929001daff7986df9/yarl-1.25.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:d21f0fa80a02d05299207eeaafef345d812ace96d5306e4ef265e1d419a615fa", size = 150071, upload-time = "2026-09-15T19:32:53.911Z" }, + { url = "https://files.pythonhosted.org/packages/64/fc/b245e448124bcda9340df38e3553fa222b50260fca027a84095e9bd8642d/yarl-1.25.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:17c9877a89fb6e2bca6f9087eb24cd7fb434653946ef5075e470d23d49b52287", size = 106780, upload-time = "2026-09-15T19:32:56.443Z" }, + { url = "https://files.pythonhosted.org/packages/51/e2/9a6ce2e334ebf218a30335ae76fb1696459430d42f733b8cb0d7d65b84d3/yarl-1.25.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:29273edf1530e397bd07cb784db1fbe0d2590b77569f2e24679a9c0a2d763b94", size = 107361, upload-time = "2026-09-15T19:32:58.827Z" }, + { url = "https://files.pythonhosted.org/packages/ed/70/66e8c76b569b450d16e190f15071c916c3df70b0e33927e415ac497cf0c2/yarl-1.25.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b7abffdf37af1cec6a2ad69b827aa84320db5894791bc8ed932dc93fb274b7e9", size = 114396, upload-time = "2026-09-15T19:33:02.24Z" }, + { url = "https://files.pythonhosted.org/packages/73/23/0d82838a05c57fdc05bc8b66e8c92dcc0df15e27463a5f163142d521c682/yarl-1.25.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2239a02249d9326655419e0168a28ca9008938eaab31dc29fc875c217927a6c0", size = 104882, upload-time = "2026-09-15T19:33:04.494Z" }, + { url = "https://files.pythonhosted.org/packages/86/d4/ea08615c4edaa6049a13a2f1128944d068d1893abda7d708d4d7ea01599a/yarl-1.25.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:664ec6a520b74a1df2810666eb67695fcb77fa663e6ea0a25aaf2e529cb24dfa", size = 119485, upload-time = "2026-09-15T19:33:06.583Z" }, + { url = "https://files.pythonhosted.org/packages/1a/82/0898bdce9b1ae403b308b9c733d0d24af4a3464270c2c081f457b16c3e0d/yarl-1.25.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4f1c91f5a5980a937ff8e238e98e6897e1ad74a4b1e2c0d68c73b5ffbb3f5c0b", size = 122490, upload-time = "2026-09-15T19:33:08.653Z" }, + { url = "https://files.pythonhosted.org/packages/d1/38/97d79b81c342b78246cfedb74809e68841f3198d21653e10d3232bd9c622/yarl-1.25.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c88edaec8c349ad4c5ad4c486a3defcc4b80ceb2f074436ffa0a87caf5e76a6", size = 115336, upload-time = "2026-09-15T19:33:11.056Z" }, + { url = "https://files.pythonhosted.org/packages/8e/9d/2577896554cd310dc470adb6da0b7dd0b435cb63e2565204a7ac240e504c/yarl-1.25.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:35dcbea443fafb3eece757ad4e514560ddeb6c34cfae1582c620d7b293d7feee", size = 111825, upload-time = "2026-09-15T19:33:13.204Z" }, + { url = "https://files.pythonhosted.org/packages/29/6b/7ac49d8ba84a5c4bd73415a4c949d22c749cb3762579b3d50e48019a78aa/yarl-1.25.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:882569ff613758cac762a457a5d72d6e211b28d4bcfea89d1d71ea942b02eac0", size = 114655, upload-time = "2026-09-15T19:33:15.553Z" }, + { url = "https://files.pythonhosted.org/packages/e5/18/e5942a16723f5b72f9b1297fd5a85a54f6300cd15c0dcb5005b90cd89156/yarl-1.25.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:d0f1489233a254bb3643d2f05de7d59019254d81daeca6b9162fe9edef57e0c7", size = 106395, upload-time = "2026-09-15T19:33:17.599Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2d/549fa46240781513ebc47ae7eb418df428a163a2a3d644cc9cbb3ecb7846/yarl-1.25.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:f41753a76f4f63927d03a0d8ba8f5ce0f2083bec29a8cfaccc55371b1564b96b", size = 119277, upload-time = "2026-09-15T19:33:19.973Z" }, + { url = "https://files.pythonhosted.org/packages/76/16/4763f78dcdc0b3b9fb3842b04afe72b9320857c6a69300c62a0eab03d119/yarl-1.25.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8fb0eb4955adf0579001581f2f71a126e8781ba61bcd120f127b0401163c6c2d", size = 112504, upload-time = "2026-09-15T19:33:22.464Z" }, + { url = "https://files.pythonhosted.org/packages/ae/b4/974e3edfe0d188393ce1cb9de400111c63fe61f4eb3b772a500d84c970d1/yarl-1.25.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:a1e32763e641a1566507d90a8d3b19bfc3cc04a9d4e5ae3e32189874ed4b58a3", size = 116243, upload-time = "2026-09-15T19:33:24.788Z" }, + { url = "https://files.pythonhosted.org/packages/b0/aa/157b940428da80c104ca09666a740e51c94963df65d5b112e06b52e4d7a8/yarl-1.25.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:65b5b2066651b7432d389e9799d979c703bcc6ef44266bb8153ef54e91e4aab3", size = 115822, upload-time = "2026-09-15T19:33:26.886Z" }, + { url = "https://files.pythonhosted.org/packages/7e/af/19fbdce41412e1b96825544cc52cd7029d3724655d0988237972f078bd29/yarl-1.25.1-cp314-cp314t-win_amd64.whl", hash = "sha256:734f6e5400352ac4254456003d462866c684703570929cff7a7bde015d0cb371", size = 107386, upload-time = "2026-09-15T19:33:29.009Z" }, + { url = "https://files.pythonhosted.org/packages/2a/99/f6431c8968e89be608d74b28ae2d024521b2953f27dd44e0dece5e04f67a/yarl-1.25.1-cp314-cp314t-win_arm64.whl", hash = "sha256:287e99ff5aa4dc1c7630bfc683ded6f106d756c99dec432a2d7f197a784f51c6", size = 102094, upload-time = "2026-09-15T19:33:31.151Z" }, + { url = "https://files.pythonhosted.org/packages/c7/3b/4f51eab40c2eabea6c3d5b121dff4b8988dc35087732ffede12d2be8b8dd/yarl-1.25.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:9b1bdaae98bc016825dd3c9d8ee1832f829b3341f9cc6ebd1a1b0a7fef7367cc", size = 143875, upload-time = "2026-09-15T19:33:33.52Z" }, + { url = "https://files.pythonhosted.org/packages/41/05/bbd58fc063f5f299a883f810760b265ca26c8167c91cb9a494d0fe2387e1/yarl-1.25.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:e7011b8fb8c4054bf0c12e5edc6cd83778b0028e99ce59b18586ed036f92cfdc", size = 104028, upload-time = "2026-09-15T19:33:36.22Z" }, + { url = "https://files.pythonhosted.org/packages/12/ee/2fba0aecb52e7020e189f684148783aa0b9cfa3b3bfb0b400646eef70ad4/yarl-1.25.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:f074e8d4aa0a5798920ddb6de3d08b228c614ff3724c3e8bd7577f4bafea867b", size = 104041, upload-time = "2026-09-15T19:33:38.86Z" }, + { url = "https://files.pythonhosted.org/packages/38/35/884beab53ed88c7247d1671972b5ef116f7351fe0c7e6de8c3558372cb16/yarl-1.25.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d42e7e3ca399555578b4d617e3a6ecf13371b3743a115995fa010c7bf341459", size = 116018, upload-time = "2026-09-15T19:33:44.265Z" }, + { url = "https://files.pythonhosted.org/packages/e2/cc/1a91b685afb55cc18608443ace95280e96e263a97565811732b6788d3269/yarl-1.25.1-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:aa4ed3dd308548f9e707d9caaf005d2d7f8c1e7868f858dfeb47fe76e16b391d", size = 107033, upload-time = "2026-09-15T19:33:46.45Z" }, + { url = "https://files.pythonhosted.org/packages/c5/c1/58b379fcb1d68d907b7fcf75200c44321896509b2a6a74abbb4b19d864d2/yarl-1.25.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:42a66563d8cc056ee32e6191e05097a7b2b3bc302e0bc3133daf8710eb18bd26", size = 123257, upload-time = "2026-09-15T19:33:48.631Z" }, + { url = "https://files.pythonhosted.org/packages/d7/2d/1fe96cf5c2aeab10095e48f38585cf5a8451fb7253234822398e52aa5336/yarl-1.25.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:98d370568f393215d605304cdb77b3d5539bd192c75b623c7304c42c8d6d8273", size = 126745, upload-time = "2026-09-15T19:33:50.999Z" }, + { url = "https://files.pythonhosted.org/packages/ad/60/8674394ce43f4dadae573a1d6f451716438e9eab7d7fe8d643c673a32d85/yarl-1.25.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23bf5b403c879a54964e0feac7285688e04bb220074878d737d331522da0a5bf", size = 117255, upload-time = "2026-09-15T19:33:53.456Z" }, + { url = "https://files.pythonhosted.org/packages/2f/72/0faa30e02605d56127d42bb987dcc97da3863b7bf70b9bfbf5f739c05e30/yarl-1.25.1-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d45673badd08456d0340e9364eddafe1c53a9d2896424294de4d7dd71ad3ee57", size = 115166, upload-time = "2026-09-15T19:33:55.669Z" }, + { url = "https://files.pythonhosted.org/packages/8c/90/9a46eac564c437e128285c5c1d7bb385d268394e9209d84f6bf4a14471ef/yarl-1.25.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:0136d640dfa9b0523853e411430a99f8a91eca85774c6420285a33b755bc6de3", size = 116082, upload-time = "2026-09-15T19:33:57.78Z" }, + { url = "https://files.pythonhosted.org/packages/ad/38/4b1a686a3758878f93d2f1ea943f5a165f3555769cd16e761cfd0efdca17/yarl-1.25.1-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:59ba3a6e1aa8cfe5adf4bd270fd965db21955401b7ca6f1696010c55ed4daec2", size = 108048, upload-time = "2026-09-15T19:34:00.25Z" }, + { url = "https://files.pythonhosted.org/packages/da/14/f348eb967f31a58348612a2b93bd8a2ab664548e2b5e879cac7f592f7201/yarl-1.25.1-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:87796fedc3ba97ec14fab55acb48584276e6c1e4c1e89c422bda62c838e754a9", size = 122775, upload-time = "2026-09-15T19:34:02.455Z" }, + { url = "https://files.pythonhosted.org/packages/ab/e9/4f7b79700f88cb9e8bb66f8b54f9bce1844c013a2c39fdc47112e9334c95/yarl-1.25.1-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:bd0912757081f89b107d6c00b2ff8a194401b0b87eadcf4481de2b865a8fd44f", size = 115096, upload-time = "2026-09-15T19:34:05.283Z" }, + { url = "https://files.pythonhosted.org/packages/cf/37/f9cb020331997d3eb887bd28d5410ecfd3d80bf163c23d7cec490d78dade/yarl-1.25.1-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:b51c159a9794633f5e0db7ecec7b2b6e3734eca1f5d17dc989ff3552a43ff78b", size = 120655, upload-time = "2026-09-15T19:34:07.382Z" }, + { url = "https://files.pythonhosted.org/packages/12/83/52fceb22891a41f168db7ec22fd1d81e06b6a0b8d9f70921bd3e785defd0/yarl-1.25.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:319e070a01db9920fb63761843f96a104c8e2b9427266731810dc1e22595b17c", size = 117488, upload-time = "2026-09-15T19:34:09.988Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5c/ce6c4ff1247fcbe4b33d462c23a097106d909b173fde7042bc52290466e2/yarl-1.25.1-cp315-cp315-win_amd64.whl", hash = "sha256:a2059a2d891bd156bc5184e7ab7a56e78a84dfcfdeac8c501b552533ad1c36ee", size = 103434, upload-time = "2026-09-15T19:34:12.56Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a1/766a906b0704fb26d52b19dc22bed48a8ba0544203b70dcf44da350e8194/yarl-1.25.1-cp315-cp315-win_arm64.whl", hash = "sha256:a78b50b4f7918a3de71105d5c0b93bbc57bb8339a4d03a9dfd449f9068e76f3d", size = 99155, upload-time = "2026-09-15T19:34:15.132Z" }, + { url = "https://files.pythonhosted.org/packages/bd/d3/a1d09b32cb6ab14f66b44939f5b4255b8b9e747aef3974af1d5d80ccc2fd/yarl-1.25.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:b5402a340723fa7da00b5cff987ddab61276be6d11251ea71ae02bcac54890d8", size = 149284, upload-time = "2026-09-15T19:34:17.452Z" }, + { url = "https://files.pythonhosted.org/packages/7f/3b/fe554d879692650bca70bfbc0df124e82e4d2bb7456f698c7756f1279a96/yarl-1.25.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:eda19ea5ee88742f47a2340816e6f2d40b53bed3ab5b69794769f36af9f35bb4", size = 106399, upload-time = "2026-09-15T19:34:21.474Z" }, + { url = "https://files.pythonhosted.org/packages/e1/4b/e7af56177ac8d40094c82d7728224c0b8472157d50d362e5fb3b014b2bc8/yarl-1.25.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:75baa6cf9b6d1c52f3e111a130e202fd8cf0a5b3a066c3f73d615e885092e4ec", size = 106968, upload-time = "2026-09-15T19:34:23.619Z" }, + { url = "https://files.pythonhosted.org/packages/da/4f/2df41fd738d46f23ef829ae8b4468d94bb6070038fc6dfab6165ed44fea8/yarl-1.25.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dbcef5a9119ef653653132cccaf999b30a0af6f33bb0a4ba80bec30056868487", size = 114717, upload-time = "2026-09-15T19:34:25.879Z" }, + { url = "https://files.pythonhosted.org/packages/69/ea/002b66df53bbd1aed1c23358ff99c9bdc744fe3f4d2740e1b2fdd7192885/yarl-1.25.1-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7efc9f082dfed77c316edffa9deb52888e1bc6789171887cc1f68e06d65465c8", size = 105198, upload-time = "2026-09-15T19:34:28.204Z" }, + { url = "https://files.pythonhosted.org/packages/b1/7c/95c8bc0c8f97d71e59c94525ad60d76f5c57d3f2820f08137ca8b9f0542a/yarl-1.25.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fe01645169a2112aa1d4ebc3e4c5f029c5c8f97adfc32e5d37c993b39a994d75", size = 120271, upload-time = "2026-09-15T19:34:30.5Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7a/6fe9da56ec77927baa669fd86c39c567ce6205bab53d581082c6744c8ae7/yarl-1.25.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1c557dfd5e3db046053a0bdc72261ade790ebe8e2c7a41b36b0ca1f14cb95f3", size = 123572, upload-time = "2026-09-15T19:34:32.73Z" }, + { url = "https://files.pythonhosted.org/packages/8b/83/35f222d17fa70a14c7c74fdf112ccf5515e0c2a87082b1f9b99f7693bf57/yarl-1.25.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ce4d6ccafb33d39bd78444612d14938ead674c25702ded2ee9c54a47735d225", size = 115228, upload-time = "2026-09-15T19:34:35.344Z" }, + { url = "https://files.pythonhosted.org/packages/da/6f/fbaaf619423578a7d898d0f226ae47bc1906c293865c416d83c17b828b0e/yarl-1.25.1-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:80a063f8297fc796296f00f100be520f209b23dc98f93ce8eba6ee7122598209", size = 111618, upload-time = "2026-09-15T19:34:37.656Z" }, + { url = "https://files.pythonhosted.org/packages/ba/78/7383278f1b3cf8e0496bd95b3281a7b09b89217b6b428db24c6b99b3deca/yarl-1.25.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:7cb414a73e21a7ab58254926073f2930cb22f5b4314ea4260a687e2b3fd4dce3", size = 114839, upload-time = "2026-09-15T19:34:40.099Z" }, + { url = "https://files.pythonhosted.org/packages/62/49/5506e5b6d29aab91bd845cc9016d88c3d3f81b81bc242b8100bdd5737825/yarl-1.25.1-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:85a18376073f8a39aa07be34f9fc77e2869aa72c55c441efdd2cf79a0407504d", size = 106212, upload-time = "2026-09-15T19:34:42.768Z" }, + { url = "https://files.pythonhosted.org/packages/46/8a/19877c193b7c5929f4b07118c18bbe390f3fadd0f59dd98c0cd12b31fa5c/yarl-1.25.1-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:77716e245c90f058466a05e6a465bb8600f767a8f4b18b4d40f3aff958e5f73c", size = 119985, upload-time = "2026-09-15T19:34:44.976Z" }, + { url = "https://files.pythonhosted.org/packages/4e/6c/0a46fbbf9ecbcbd0cc20d2193394254b9e19817f22c814aab60f99847400/yarl-1.25.1-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:1e80dcf1446e1b080b1932b0d103c464a04112f5bc31f0f983ad418172063cde", size = 112081, upload-time = "2026-09-15T19:34:47.45Z" }, + { url = "https://files.pythonhosted.org/packages/ef/30/93f5d471230c74ccd06255d0842739f86551f937f9e63a5e947853c6244a/yarl-1.25.1-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:bdc8d8b8c22e9e43ac68316b5e6cf083dec537f4ec213cb4aa967b583bc3fa64", size = 116995, upload-time = "2026-09-15T19:34:49.972Z" }, + { url = "https://files.pythonhosted.org/packages/2b/80/c386593035ee3f9c6c6af0847b5578f2830c674794a9d7701b744a3ebd42/yarl-1.25.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:dfbf531053a0935f2e871bcd4753f90313688772ff8c017f5ea402e315a78c1f", size = 115570, upload-time = "2026-09-15T19:34:52.628Z" }, + { url = "https://files.pythonhosted.org/packages/38/02/eef443559563ef8f2e10469387b8b1e97cb5efee95b288a56da60801f7ee/yarl-1.25.1-cp315-cp315t-win_amd64.whl", hash = "sha256:b13b88747769537f3d32e89e3a735da10c0a9e35d7322928c701b5f93d3afffd", size = 106811, upload-time = "2026-09-15T19:34:54.935Z" }, + { url = "https://files.pythonhosted.org/packages/88/91/41e284ca2cf5211e05dae031d126a3668aea88fa759df56e7e35c6ad25ba/yarl-1.25.1-cp315-cp315t-win_arm64.whl", hash = "sha256:783dd1467083f4d3f7722ad6a313f24c173e7571372738fcb7a6e6d1ba48df25", size = 101804, upload-time = "2026-09-15T19:34:57.231Z" }, + { url = "https://files.pythonhosted.org/packages/54/22/318c7980066769c6bcd9221ed2248294f5698811da099013098c670565ed/yarl-1.25.1-py3-none-any.whl", hash = "sha256:681c758b0490f9e96b78e5fa8e8dc6e648e9185bb6eaebe73183c33ea0c445f3", size = 63617, upload-time = "2026-09-15T19:34:59.616Z" }, +] + [[package]] name = "zensical" version = "0.0.50" From 9dd4f5278c60a8c6447acaef499f3ad3068a063c Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Thu, 17 Sep 2026 12:50:08 +0200 Subject: [PATCH 2/3] Preserve runtime branch tracing on Python 3.10 and 3.11 Return explicitly after successful transport cleanup and check peer request IDs after closing the inner client context. This preserves behavior while avoiding unreported async-exit branch arcs on older interpreters. --- src/mcp/server/runtime.py | 1 + tests/server/test_runtime.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/mcp/server/runtime.py b/src/mcp/server/runtime.py index 79490cdab9..9e67577eca 100644 --- a/src/mcp/server/runtime.py +++ b/src/mcp/server/runtime.py @@ -150,6 +150,7 @@ def started(self, value: None = None) -> None: logger.warning("Transport cleanup exceeded five seconds") if run_error is not None: raise run_error + return except Exception: if not ready: raise diff --git a/tests/server/test_runtime.py b/tests/server/test_runtime.py index a94a720987..466f8d6b13 100644 --- a/tests/server/test_runtime.py +++ b/tests/server/test_runtime.py @@ -121,8 +121,8 @@ async def call(client: Client, name: str) -> None: async with anyio.create_task_group() as tg: tg.start_soon(call, alice, "alice") tg.start_soon(call, bob, "bob") - if modes[0] == modes[1]: - assert request_ids["alice"] == request_ids["bob"] + if modes[0] == modes[1]: + assert request_ids["alice"] == request_ids["bob"] await call(alice, "alice") assert lifecycle == ["startup"] assert lifecycle == ["startup", "shutdown"] From 99306166ec4c507ff312e9484b995e203ac868d5 Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Thu, 17 Sep 2026 15:37:19 +0200 Subject: [PATCH 3/3] Separate network adapters from the SDK transport API review --- TRANSPORT_API_PLAN.md | 26 +- docs/advanced/low-level-server.md | 2 +- docs/client/transports.md | 2 +- examples/transports/README.md | 120 --- examples/transports/brokers/mosquitto.acl | 11 - examples/transports/brokers/mosquitto.conf | 10 - .../brokers/rabbitmq-definitions.json | 13 - examples/transports/brokers/rabbitmq.conf | 2 - examples/transports/compose.yaml | 49 - examples/transports/demo_amqp.py | 38 - examples/transports/demo_common.py | 93 -- examples/transports/demo_grpc.py | 71 -- examples/transports/demo_grpc_features.py | 91 -- examples/transports/demo_mqtt.py | 39 - .../mcp_transport_examples/__init__.py | 1 - .../mcp_transport_examples/_grpc_codec.py | 35 - .../transports/mcp_transport_examples/amqp.py | 138 --- .../transports/mcp_transport_examples/grpc.py | 38 - .../mcp_transport_examples/grpc_client.py | 139 --- .../mcp_transport_examples/grpc_context.py | 64 -- .../mcp_transport_examples/grpc_response.py | 58 -- .../mcp_transport_examples/grpc_server.py | 158 ---- .../transports/mcp_transport_examples/mqtt.py | 125 --- .../mcp_transport_examples/py.typed | 0 .../mcp_transport_examples/rpc.proto | 27 - .../mcp_transport_examples/rpc_pb2.py | 42 - .../mcp_transport_examples/rpc_pb2.pyi | 36 - examples/transports/pyproject.toml | 68 -- .../reproduce_grpc_loop_shutdown.py | 37 - examples/transports/tests/__init__.py | 1 - ...ive_error_keeps_code_message_and_data.yaml | 19 - ...s_large_integers_and_extension_fields.yaml | 19 - ..._reaches_the_client_before_the_result.yaml | 19 - examples/transports/tests/conftest.py | 14 - examples/transports/tests/test_amqp.py | 30 - examples/transports/tests/test_grpc.py | 185 ---- .../tests/test_grpc_cancel_signal.py | 72 -- examples/transports/tests/test_grpc_client.py | 106 --- .../tests/test_grpc_client_shutdown.py | 94 -- .../transports/tests/test_grpc_context.py | 51 -- .../transports/tests/test_grpc_lifecycle.py | 95 -- .../transports/tests/test_grpc_response.py | 84 -- examples/transports/tests/test_grpc_server.py | 220 ----- .../tests/test_grpc_shutdown_order.py | 93 -- examples/transports/tests/test_grpc_tls.py | 177 ---- examples/transports/tests/test_mqtt.py | 28 - pyproject.toml | 4 +- src/mcp/server/runner.py | 6 +- src/mcp/server/sse.py | 6 +- src/mcp/server/streamable_http.py | 4 + src/mcp/server/streamable_http_manager.py | 9 +- src/mcp/shared/direct_dispatcher.py | 2 +- src/mcp/shared/dispatcher.py | 5 +- src/mcp/shared/jsonrpc_dispatcher.py | 4 +- src/mcp/shared/message.py | 6 +- tests/docs_src/test_authorization.py | 74 +- tests/server/test_runtime.py | 26 +- tests/shared/test_dispatcher.py | 12 +- tests/shared/test_message.py | 14 + tests/shared/test_sse.py | 6 +- tests/shared/test_streamable_http.py | 8 +- uv.lock | 847 +----------------- 62 files changed, 190 insertions(+), 3683 deletions(-) delete mode 100644 examples/transports/README.md delete mode 100644 examples/transports/brokers/mosquitto.acl delete mode 100644 examples/transports/brokers/mosquitto.conf delete mode 100644 examples/transports/brokers/rabbitmq-definitions.json delete mode 100644 examples/transports/brokers/rabbitmq.conf delete mode 100644 examples/transports/compose.yaml delete mode 100644 examples/transports/demo_amqp.py delete mode 100644 examples/transports/demo_common.py delete mode 100644 examples/transports/demo_grpc.py delete mode 100644 examples/transports/demo_grpc_features.py delete mode 100644 examples/transports/demo_mqtt.py delete mode 100644 examples/transports/mcp_transport_examples/__init__.py delete mode 100644 examples/transports/mcp_transport_examples/_grpc_codec.py delete mode 100644 examples/transports/mcp_transport_examples/amqp.py delete mode 100644 examples/transports/mcp_transport_examples/grpc.py delete mode 100644 examples/transports/mcp_transport_examples/grpc_client.py delete mode 100644 examples/transports/mcp_transport_examples/grpc_context.py delete mode 100644 examples/transports/mcp_transport_examples/grpc_response.py delete mode 100644 examples/transports/mcp_transport_examples/grpc_server.py delete mode 100644 examples/transports/mcp_transport_examples/mqtt.py delete mode 100644 examples/transports/mcp_transport_examples/py.typed delete mode 100644 examples/transports/mcp_transport_examples/rpc.proto delete mode 100644 examples/transports/mcp_transport_examples/rpc_pb2.py delete mode 100644 examples/transports/mcp_transport_examples/rpc_pb2.pyi delete mode 100644 examples/transports/pyproject.toml delete mode 100644 examples/transports/reproduce_grpc_loop_shutdown.py delete mode 100644 examples/transports/tests/__init__.py delete mode 100644 examples/transports/tests/cassettes/test_grpc/test_native_error_keeps_code_message_and_data.yaml delete mode 100644 examples/transports/tests/cassettes/test_grpc/test_native_payload_keeps_large_integers_and_extension_fields.yaml delete mode 100644 examples/transports/tests/cassettes/test_grpc/test_native_progress_reaches_the_client_before_the_result.yaml delete mode 100644 examples/transports/tests/conftest.py delete mode 100644 examples/transports/tests/test_amqp.py delete mode 100644 examples/transports/tests/test_grpc.py delete mode 100644 examples/transports/tests/test_grpc_cancel_signal.py delete mode 100644 examples/transports/tests/test_grpc_client.py delete mode 100644 examples/transports/tests/test_grpc_client_shutdown.py delete mode 100644 examples/transports/tests/test_grpc_context.py delete mode 100644 examples/transports/tests/test_grpc_lifecycle.py delete mode 100644 examples/transports/tests/test_grpc_response.py delete mode 100644 examples/transports/tests/test_grpc_server.py delete mode 100644 examples/transports/tests/test_grpc_shutdown_order.py delete mode 100644 examples/transports/tests/test_grpc_tls.py delete mode 100644 examples/transports/tests/test_mqtt.py create mode 100644 tests/shared/test_message.py diff --git a/TRANSPORT_API_PLAN.md b/TRANSPORT_API_PLAN.md index e462bd6d92..580210a3e7 100644 --- a/TRANSPORT_API_PLAN.md +++ b/TRANSPORT_API_PLAN.md @@ -2,6 +2,10 @@ Status: implementation in progress. New APIs still need final compatibility and native-binding review before release. +## Review layout + +The review stack separates the SDK transport APIs, native gRPC adapter, MQTT adapter, and AMQP adapter into four pull requests, in that order. This document tracks the whole initiative; the SDK pull request contains no network adapter implementation or optional adapter dependencies. Each adapter pull request targets the preceding branch so its diff contains only that layer. + ## Progress | Work | Current evidence | Remaining | @@ -252,7 +256,27 @@ The native adapter now exposes gRPC's verified `peer_identity_key` and immutable TLS validation exposed a gRPC completion-queue limitation, reproduced without MCP in `examples/transports/reproduce_grpc_loop_shutdown.py`. With `grpcio==1.84.0` on macOS/Python 3.14.6, cancelled connectivity watches can complete after `channel.close()` and target a previously closed event loop. The adapter suite keeps one AnyIO runner for its session, while still closing per-test resources. The README records this support restriction, not an upstream fix; repeated loop lifetimes and final native-queue drainage are not certified. -Current evidence is in `/tmp/mcp-core-final.log`, `/tmp/mcp-adapter-final310.log`, `/tmp/mcp-adapter-final314.log`, `/tmp/mcp-docs-final.log`, and `/tmp/mcp-conformance-final-{client,server}-*.log`. Coverage data uses `/tmp/mcp-adapter-final310` and `/tmp/mcp-adapter-final314`. Core coverage is 100%; total adapter coverage is 90%, with only MQTT/AMQP implementation gaps remaining. Generated protobuf implementation is excluded as compiler output, not handwritten adapter code. +### Reproduce the validation + +```bash +./scripts/test +UV_PROJECT_ENVIRONMENT=examples/transports/.venv uv sync --frozen --package mcp-transport-examples --group dev +UV_PROJECT_ENVIRONMENT=examples/transports/.venv uv run --frozen --package mcp-transport-examples --group dev pytest -c examples/transports/pyproject.toml examples/transports/tests --record-mode=none +uv run --frozen pyright --project examples/transports +DOCS_LANGUAGES=en-only bash scripts/docs/build.sh +``` + +Run the broker commands in `examples/transports/README.md` against the pinned Compose fixtures. The shared-check workflow now runs the complete adapter suite and live broker programs on Python 3.10 and 3.14, and retains `transport-results-` JUnit artifacts. Results are attached to [the pull request's checks](https://github.com/modelcontextprotocol/python-sdk/pull/3517/checks), not machine-local log paths. The conformance workflow records all six baseline legs separately. + +Core coverage remains 100%. Whole adapter coverage is still incomplete because MQTT/AMQP failure paths are not cassette-backed; generated protobuf implementation is excluded as compiler output, not handwritten adapter code. Do not interpret a passing adapter pytest job as completion of that separate coverage gate. + +### Review corrections + +Native regressions now cover swallowed direct-handler cancellation, notification callback isolation, post-close notification drops, late request-scoped notifications, sanitized raw-dispatcher errors, strict progress fields, deep JSON and exponent overflow. HTTP framing supplies handler-visible context and headers without adding credentials to message representations; driver stream cleanup is shielded and bounded. The published principal-binding example is exercised directly. + +RabbitMQ no longer grants client writes to the default exchange. Each receiving queue has a dedicated direct exchange, and live checks reject both default-exchange injection and writes to another principal's exchange. MQTT examples configure Last Wills before CONNECT; a live broker-forced disconnect settles a pending MCP call without relying on its request timeout. The existing aiomqtt negative-publish-reason limitation remains a merge blocker: its public API does not expose those acknowledgement codes. + +Optional runtime design feedback remains separate from these corrections: configurable cleanup grace, exception-group behavior, and admission cancellation during runtime shutdown need a contract decision rather than an unreviewed change in semantics. ## Next implementation work diff --git a/docs/advanced/low-level-server.md b/docs/advanced/low-level-server.md index 01da00e3ed..d8181f1a9a 100644 --- a/docs/advanced/low-level-server.md +++ b/docs/advanced/low-level-server.md @@ -69,7 +69,7 @@ In a test you skip uvicorn and the port: `Client(server)` takes a low-level `Ser `Server.serve()` shares one application lifespan across multiple custom transport connections, just like `MCPServer.serve()`. Use the complete adapter example under [Running your server](../run/index.md#custom-transports). -For a single connection, `Server.run(read_stream, write_stream, initialization_options, *, transport_builder=...)` remains available. The optional builder converts inbound message metadata into the `TransportContext` exposed as `ctx.transport`. Without it, stream dispatch supplies generic JSON-RPC metadata. Both paths retain the existing protocol-version handling; custom transport capabilities cannot enable features that the negotiated version forbids. +For a single connection, `Server.run(read_stream, write_stream, initialization_options, *, transport_builder=...)` remains available. The optional builder converts inbound message metadata into the `TransportContext` exposed as `ctx.transport`. Without it, stream dispatch uses the context supplied by the framing transport, falling back to generic JSON-RPC metadata. Built-in HTTP transports supply their kind and the current request's headers. Both paths retain the existing protocol-version handling; custom transport capabilities cannot enable features that the negotiated version forbids. ## Nothing is checked for you diff --git a/docs/client/transports.md b/docs/client/transports.md index 010c964a96..97050799a0 100644 --- a/docs/client/transports.md +++ b/docs/client/transports.md @@ -157,7 +157,7 @@ The server side of this example uses `server.serve()`. Its lifecycle and connect `DispatcherTransport` explicitly wraps an async context manager yielding a `Dispatcher`. `Client` enters that context, starts the dispatcher, and uses its ordinary MCP negotiation, callbacks, caching, and validation. It stops the dispatcher before exiting the connection context. You configure the client through the same constructor; there is no separate native client-session API. -The example uses the SDK's `DirectDispatcher`. The repository's `examples/transports/README.md` also contains a real gRPC implementation with protobuf envelopes and JSON payloads. Native network bindings implement this dispatcher boundary instead of creating `SessionMessage` streams. The connection context acquires the transport resources; it must yield an unstarted dispatcher because the SDK owns `run()`. +The example uses the SDK's `DirectDispatcher`. The native gRPC reference adapter is developed in a separate follow-up to this SDK API change. Native network bindings implement this dispatcher boundary instead of creating `SessionMessage` streams. The connection context acquires the transport resources; it must yield an unstarted dispatcher because the SDK owns `run()`. On the server, `runtime.connect(DispatcherTransport(...))` serves the modern per-request-envelope protocol. It rejects the legacy initialize handshake. Use `mode="auto"` or a supported modern version on the client. Message transports still support both eras. Native dispatchers supply their own contexts, so this server path rejects `session_id=` and `transport_builder=`. diff --git a/examples/transports/README.md b/examples/transports/README.md deleted file mode 100644 index d8e06446bf..0000000000 --- a/examples/transports/README.md +++ /dev/null @@ -1,120 +0,0 @@ -# Reference custom transports - -```bash -docker compose -p mcp-sdk-transport-check -f examples/transports/compose.yaml up --wait -UV_PROJECT_ENVIRONMENT=examples/transports/.venv uv sync --frozen --package mcp-transport-examples --group dev -UV_PROJECT_ENVIRONMENT=examples/transports/.venv uv run --frozen --package mcp-transport-examples python examples/transports/demo_mqtt.py -UV_PROJECT_ENVIRONMENT=examples/transports/.venv uv run --frozen --package mcp-transport-examples python examples/transports/demo_amqp.py -docker compose -p mcp-sdk-transport-check -f examples/transports/compose.yaml down --volumes -``` - -Run these commands from the repository root. Both programs exit successfully only after checking concurrent calls for two peers, both `Server` and `MCPServer`, and `legacy`, `auto`, and pinned `2026-07-28` clients. The server lifespan starts once per run, not once per peer. - -The adapters import only public SDK APIs. They live in a separate workspace package so installing `mcp` does not install MQTT or AMQP dependencies. They are reference implementations under development, not production-ready transports or official MCP wire bindings. The native gRPC binding is described below. - -## Native gRPC - -```bash -UV_PROJECT_ENVIRONMENT=examples/transports/.venv uv run --frozen --package mcp-transport-examples python examples/transports/demo_grpc.py -UV_PROJECT_ENVIRONMENT=examples/transports/.venv uv run --frozen --package mcp-transport-examples python examples/transports/demo_grpc_features.py -UV_PROJECT_ENVIRONMENT=examples/transports/.venv uv run --frozen --package mcp-transport-examples --group dev pytest -c examples/transports/pyproject.toml examples/transports/tests/test_grpc*.py --record-mode=none -``` - -These programs start their own loopback gRPC listener. No broker is needed. `grpc_client(channel)` and `grpc_server(listener)` return `DispatcherTransport` objects for the existing client and server runtime APIs. You own the channel and listener; runtime shutdown stops MCP handlers without taking ownership of other gRPC services on the listener. - -Register the binding through `runtime.connect(grpc_server(listener))` before starting the listener. Use one MCP binding per gRPC server. The server adapter rejects excess work at `max_requests`, which defaults to 64, rather than queuing unlimited waiting handlers. - -The binding serves modern per-request MCP envelopes. It has no legacy initialize handshake. Each MCP request is a native server-streaming RPC on `/mcp.transport.example.MCP/Call`; gRPC correlates calls and provides deadlines and cancellation. The auxiliary request ID supports MCP subscription correlation and stays local to each client's calls. - -`mcp_transport_examples/rpc.proto` defines protobuf envelopes with JSON-encoded parameters, results, and error data. There is no JSON-RPC envelope. JSON payloads preserve arbitrary extension fields and integer precision, which protobuf `Struct` would otherwise lose through its floating-point number representation. This is an example binding, not compatibility with another project's gRPC schema. - -Notifications precede one terminal result or error, followed by end-of-stream. Progress, subscription acknowledgments, and change events use the originating RPC's response stream. Unsolicited notifications without a request channel are unsupported. Ordinary MCP errors preserve their code, message, and data. Native deadline failures become `REQUEST_TIMEOUT`; other gRPC failures become `CONNECTION_CLOSED` with the original status exception as their cause. - -The examples and regression tests check both server APIs, progress, subscriptions, multi-round-trip results, concurrent clients with colliding request IDs, caller cancellation, deadlines, runtime shutdown, borrowed-channel closure, and client shutdown during a blocked callback. Cancellation is signalled before handler cleanup begins. Active handlers and callbacks are joined before their owning resources close, including shielded cleanup that takes longer than five seconds. Code that ignores cancellation indefinitely can therefore hold shutdown indefinitely; enforce a hard process deadline in your supervisor rather than closing resources under running code. The SDK's five-second transport and application cleanup deadlines do not replace this join. Invocation metadata and socket peer addresses are not authenticated principals. - -Regenerate the protobuf bindings with the pinned compiler: - -```bash -UV_PROJECT_ENVIRONMENT=examples/transports/.venv uv run --frozen --package mcp-transport-examples --group dev python -m grpc_tools.protoc --proto_path=examples/transports --python_out=examples/transports --pyi_out=examples/transports examples/transports/mcp_transport_examples/rpc.proto -``` - -### TLS and peer identity - -```bash -UV_PROJECT_ENVIRONMENT=examples/transports/.venv uv run --frozen --package mcp-transport-examples --group dev pytest -c examples/transports/pyproject.toml examples/transports/tests/test_grpc_tls.py --record-mode=none -``` - -These tests create temporary certificate authorities and real local TLS endpoints. They check mutual TLS, server-only TLS, and plaintext connections. Missing or untrusted client certificates cannot reach MCP middleware when the listener requires client authentication. Forged MCP client information and gRPC invocation metadata do not change the verified identity. - -Configure TLS through your gRPC channel and listener credentials. Set `require_client_auth=True` on `grpc.ssl_server_credentials()` when clients must present a certificate. Handlers receive `GRPCContext.peer_identity_key` and `peer_identities` from gRPC's native authentication context. Without client authentication, these are `None` and an empty tuple, including on encrypted server-only TLS connections. - -Certificate validation is not application authorization. The identity values do not identify their issuing authority. If you trust independent authorities that can issue the same common name or subject alternative name, do not treat that name as a globally unique principal. Choose a trust-domain namespace and certificate-issuance policy before using these values with `RequestStateSecurity.bind_principal`. An issuer-name string or untrusted invocation metadata cannot supply that trust boundary. - -### Event-loop lifetime - -```bash -UV_PROJECT_ENVIRONMENT=examples/transports/.venv uv run --frozen --package mcp-transport-examples python examples/transports/reproduce_grpc_loop_shutdown.py -``` - -This diagnostic intentionally fails when a native completion targets a closed loop. It reproduces the limitation without importing MCP. The failure was observed with `grpcio==1.84.0` on macOS and Python 3.14.6; the assertion includes the runtime versions. - -Keep one long-lived asyncio event loop per process or test worker. Create, use, and close all gRPC resources on that loop. Repeated `anyio.run()` or `asyncio.run()` lifetimes are outside this adapter's current support. gRPC's process-wide completion queue can deliver cancelled connectivity-watch callbacks after `channel.close()` returns. Joining MCP handlers does not drain those native callbacks. The adapter tests keep one AnyIO runner alive for the session; they do not suppress loop errors or claim an upstream correction. - -## Connection ownership - -`demo_mqtt.py` and `demo_amqp.py` contain complete adapter setup. `demo_common.py` runs the same MCP application through either transport. - -Each side owns and enters its network client or AMQP channel before entering the transport. Keep the server's network resources alive until `server.serve()` exits. The transport unsubscribes or cancels its consumer, but does not close a borrowed client or channel. - -The examples provision a dedicated network connection per logical peer. Both sides agree on a fresh session identifier out of band. They do not implement service discovery, dynamic peer acceptance, or multiplexing many peers through one MQTT messages iterator. Those choices belong in an adapter, not in the SDK dispatcher. - -## Wire bindings - -| Property | MQTT 5 | AMQP 0.9.1 | -| --- | --- | --- | -| Requests | `mcp///requests` | `mcp...requests` | -| Replies | `mcp///responses` | `mcp...responses` | -| Framing | One JSON-RPC message per publish | One JSON-RPC message per delivery; `application/json` content type | -| Delivery | QoS 2 only | Publisher confirmations; acknowledge before SDK handoff | -| Close | Empty payload | Empty JSON-typed message body | -| Retention | Never retain; do not receive stored retained messages | Nondurable, auto-delete queues | -| Expiry | MQTT message expiry, default 60 seconds | Message TTL and unused queue expiry, default 60 seconds | -| Duplicate handling | MQTT QoS 2 handles protocol retransmissions within its session | Reject redeliveries without requeue | -| Reconnect | Fail pending work; establish a fresh logical connection | Fail pending work; establish a fresh logical connection | - -Always use fresh topics or queue names after reconnecting. Do not reuse JSON-RPC request IDs across multiple peers in one SDK stream pair. Server-initiated messages, progress, and cancellation use the same pair of directions; the SDK applies protocol-version restrictions. - -Both adapters reject messages larger than `max_message_size`, which defaults to 4 MiB. Malformed messages become recoverable stream exceptions. Connection loss closes the receive stream so the SDK can fail pending calls. - -## Delivery limits - -QoS 2 and publisher confirmations describe broker delivery, not exactly-once tool execution. Republishing a JSON-RPC request is a new delivery and can repeat a side effect. Neither adapter retries calls automatically. - -The AMQP adapter deliberately acknowledges before handing a message to the SDK. A process failure in that window can lose work. Rejecting broker redeliveries avoids automatically rerunning uncertain work, but is not a replacement for application idempotency. - -RabbitMQ queues hold at most 256 ready messages and reject publication on overflow. Consumer prefetch bounds unacknowledged deliveries. The MQTT example bounds aiomqtt's incoming queue at 256 messages; aiomqtt can drop messages when that queue fills, so configure client request timeouts and monitor its overflow warnings. This remains a limitation to resolve before claiming reliable saturation behavior. Its publish callback also discards MQTT negative reason codes, so broker rejection may surface only as an MCP request timeout. Neither setting bounds the number of concurrently executing tool handlers. - -## Authentication - -The local brokers are configured with per-user topic or queue permissions. RabbitMQ cross-peer response consumption was also checked live and rejected. Mosquitto can acknowledge a subscription even when its ACL prevents delivery, so a successful SUBACK is not proof of permission. MQTT authorization-denial checks remain to be automated. The server attaches the principal associated with its configured route; it does not accept an arbitrary reply destination from the message payload. - -For production, use TLS and your broker's credential and authorization policy. Include the issuing authority and user in a stable principal identifier. Use `RequestStateSecurity.bind_principal` to bind multi-round-trip state to verified metadata; the SDK does not automatically convert broker identity into an HTTP OAuth token. - -The Compose fixtures use public test credentials, listen only on localhost, and disable durable storage. Do not deploy these broker configurations. You can change the local ports with `MQTT_TEST_PORT` and `AMQP_TEST_PORT`; the defaults are 13883 and 15672 respectively. - -Both libraries use asyncio. The MQTT provider requires a selector event loop on Windows. Trio and Windows adapter validation have not been completed. - -## Validation status - -```bash -UV_PROJECT_ENVIRONMENT=examples/transports/.venv uv run --frozen --package mcp-transport-examples --group dev pytest -c examples/transports/pyproject.toml examples/transports/tests --record-mode=none -uv run --frozen pyright --project examples/transports -``` - -The broker unit tests check configuration failures without opening network connections. The broker programs verify real traffic against Mosquitto and RabbitMQ; they are not cassette-backed CI coverage. - -The gRPC tests record real calls with `cassetter` and replay with `--record-mode=none`. They check payload fidelity, progress, and application errors. The tests also compare the serialized requests with the recording: the current gRPC matcher matches only the RPC method, which is insufficient for a generic MCP binding. Each cassette contains one RPC to avoid replaying a newly recorded call as the response to a different request during recording. - -The lifecycle, capacity, and malformed-frame regression tests own a gRPC server inside the test process. That server is the software under test, not an external service; replaying its outputs would bypass the behavior being checked. The cassette tests separately compare recorded native results with the current in-process MCP handler and verify request-body fidelity. `cassetter` also lacks parts of the streaming-call cancellation interface, so it is not used to stand in for these live server tests. Binary protobuf payloads are not pattern-scrubbed; inspect new cassettes before committing them. The checked-in recordings contain only public test data. - -`cassetter` has no MQTT or AMQP interceptor. Broker record/replay, complete broker-adapter coverage, broker TLS, and cross-platform checks remain open gates. The gRPC implementation and its regression tests have full line and branch coverage on the locally checked interpreters; that does not establish interoperability with another binding or support for repeated event-loop lifetimes. Do not treat a successful live program or cassette replay as evidence for untested server behavior. diff --git a/examples/transports/brokers/mosquitto.acl b/examples/transports/brokers/mosquitto.acl deleted file mode 100644 index 2872feb502..0000000000 --- a/examples/transports/brokers/mosquitto.acl +++ /dev/null @@ -1,11 +0,0 @@ -user server -topic read mcp/+/+/requests -topic write mcp/+/+/responses - -user alice -topic write mcp/alice/+/requests -topic read mcp/alice/+/responses - -user bob -topic write mcp/bob/+/requests -topic read mcp/bob/+/responses diff --git a/examples/transports/brokers/mosquitto.conf b/examples/transports/brokers/mosquitto.conf deleted file mode 100644 index 751d1a2784..0000000000 --- a/examples/transports/brokers/mosquitto.conf +++ /dev/null @@ -1,10 +0,0 @@ -listener 1883 -allow_anonymous false -password_file /tmp/passwords -acl_file /mosquitto/config/mosquitto.acl -persistence false -log_dest stdout -log_type all -max_packet_size 4195328 -max_inflight_messages 32 -max_queued_messages 256 diff --git a/examples/transports/brokers/rabbitmq-definitions.json b/examples/transports/brokers/rabbitmq-definitions.json deleted file mode 100644 index 03811d0fb8..0000000000 --- a/examples/transports/brokers/rabbitmq-definitions.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "users": [ - {"name": "server", "password_hash": "dGVzdISQMYLFRDkVmIyH/2iPR3elfgPHcZO7uFXGqTE0UeU9", "hashing_algorithm": "rabbit_password_hashing_sha256", "tags": []}, - {"name": "alice", "password_hash": "dGVzdMhyAjoCEMSETc4cmiL+/OInoknje5+9BVEZ8SaVlj+Z", "hashing_algorithm": "rabbit_password_hashing_sha256", "tags": []}, - {"name": "bob", "password_hash": "dGVzdFu3/YOufljrGKLUVvAqIh2R9WeQ87Ql039pWvRL9HZO", "hashing_algorithm": "rabbit_password_hashing_sha256", "tags": []} - ], - "vhosts": [{"name": "/"}], - "permissions": [ - {"user": "server", "vhost": "/", "configure": ".*", "write": ".*", "read": ".*"}, - {"user": "alice", "vhost": "/", "configure": "^mcp\\.alice\\..*", "write": "^(amq\\.default|mcp\\.alice\\.[^.]+\\.requests)$", "read": "^mcp\\.alice\\.[^.]+\\.responses$"}, - {"user": "bob", "vhost": "/", "configure": "^mcp\\.bob\\..*", "write": "^(amq\\.default|mcp\\.bob\\.[^.]+\\.requests)$", "read": "^mcp\\.bob\\.[^.]+\\.responses$"} - ] -} diff --git a/examples/transports/brokers/rabbitmq.conf b/examples/transports/brokers/rabbitmq.conf deleted file mode 100644 index 57c11bfd94..0000000000 --- a/examples/transports/brokers/rabbitmq.conf +++ /dev/null @@ -1,2 +0,0 @@ -definitions.import_backend = local_filesystem -definitions.local.path = /etc/rabbitmq/definitions.json diff --git a/examples/transports/compose.yaml b/examples/transports/compose.yaml deleted file mode 100644 index 26c1fa1b32..0000000000 --- a/examples/transports/compose.yaml +++ /dev/null @@ -1,49 +0,0 @@ -services: - amqp: - image: rabbitmq:4.1.8-alpine@sha256:1a087dd3a29b91448407409df70f4f6cb213ac0c269a62861bfe2a665f4ced03 - ports: - - "127.0.0.1:${AMQP_TEST_PORT:-15672}:5672" - volumes: - - ./brokers/rabbitmq.conf:/etc/rabbitmq/rabbitmq.conf:ro - - ./brokers/rabbitmq-definitions.json:/etc/rabbitmq/definitions.json:ro - healthcheck: - test: ["CMD", "rabbitmq-diagnostics", "-q", "check_port_connectivity"] - interval: 2s - timeout: 5s - retries: 30 - mqtt: - image: eclipse-mosquitto:2.0.22@sha256:212f89e1eaeb2c322d6441b64396e3346026674db8fa9c27beac293405c32b3c - ports: - - "127.0.0.1:${MQTT_TEST_PORT:-13883}:1883" - volumes: - - ./brokers/mosquitto.conf:/mosquitto/config/mosquitto.conf:ro - - ./brokers/mosquitto.acl:/mosquitto/config/mosquitto.acl:ro - entrypoint: ["/bin/sh", "-ec"] - command: - - | - mosquitto_passwd -b -c /tmp/passwords server test-server-password - mosquitto_passwd -b /tmp/passwords alice test-alice-password - mosquitto_passwd -b /tmp/passwords bob test-bob-password - chmod 644 /tmp/passwords - exec mosquitto -c /mosquitto/config/mosquitto.conf - healthcheck: - test: - [ - "CMD", - "mosquitto_pub", - "-h", - "127.0.0.1", - "-u", - "server", - "-P", - "test-server-password", - "-t", - "mcp/alice/health/responses", - "-m", - "", - "-q", - "2", - ] - interval: 1s - timeout: 3s - retries: 20 diff --git a/examples/transports/demo_amqp.py b/examples/transports/demo_amqp.py deleted file mode 100644 index 828e3f33a9..0000000000 --- a/examples/transports/demo_amqp.py +++ /dev/null @@ -1,38 +0,0 @@ -"""Exercise the AMQP 0.9.1 adapter against the local RabbitMQ broker.""" - -import os -from contextlib import AsyncExitStack - -import aio_pika -import anyio -from mcp.shared.transport import Transport - -from demo_common import verify -from mcp_transport_examples.amqp import amqp_transport - - -async def open_transport(stack: AsyncExitStack, principal: str, session: str, server_side: bool) -> Transport: - user = "server" if server_side else principal - connection = await aio_pika.connect( - host="127.0.0.1", - port=int(os.environ.get("AMQP_TEST_PORT", "15672")), - login=user, - password=f"test-{user}-password", - ) - await stack.enter_async_context(connection) - channel = await connection.channel(publisher_confirms=True) - stack.push_async_callback(channel.close) - queue = f"mcp.{principal}.{session}" - incoming, outgoing = ("requests", "responses") if server_side else ("responses", "requests") - return amqp_transport(channel, incoming_queue=f"{queue}.{incoming}", outgoing_queue=f"{queue}.{outgoing}") - - -async def main() -> None: - for highlevel in (False, True): - for mode in ("legacy", "auto", "2026-07-28"): - with anyio.fail_after(5): - await verify(open_transport, kind="amqp", highlevel=highlevel, mode=mode) - - -if __name__ == "__main__": - anyio.run(main) diff --git a/examples/transports/demo_common.py b/examples/transports/demo_common.py deleted file mode 100644 index 58a2b4af64..0000000000 --- a/examples/transports/demo_common.py +++ /dev/null @@ -1,93 +0,0 @@ -"""Shared live-broker checks for the reference adapters.""" - -from collections.abc import AsyncIterator, Awaitable, Callable -from contextlib import AsyncExitStack, asynccontextmanager -from dataclasses import dataclass -from functools import partial -from typing import Any, TypeAlias -from uuid import uuid4 - -import anyio -from mcp import Client -from mcp.server import Server, ServerRequestContext -from mcp.server.mcpserver import Context, MCPServer -from mcp.shared.transport import MessageMetadata, Transport, TransportContext -from mcp.types import CallToolRequestParams, CallToolResult, ListToolsResult, PaginatedRequestParams, TextContent, Tool - -TransportFactory: TypeAlias = Callable[[AsyncExitStack, str, str, bool], Awaitable[Transport]] - - -@dataclass(kw_only=True, frozen=True) -class BrokerContext(TransportContext): - principal: str - - -def peer_context(metadata: MessageMetadata, *, principal: str, kind: str) -> BrokerContext: - return BrokerContext(kind=kind, can_send_request=True, principal=principal) - - -async def verify(factory: TransportFactory, *, kind: str, highlevel: bool, mode: str) -> None: - """Check real concurrent calls, peer metadata, both server APIs, and shared lifespan.""" - entered = {"alice": anyio.Event(), "bob": anyio.Event()} - lifecycle: list[str] = [] - - async def identity(transport: TransportContext | None) -> str: - assert isinstance(transport, BrokerContext) - principal = transport.principal - entered[principal].set() - await entered["bob" if principal == "alice" else "alice"].wait() - return principal - - @asynccontextmanager - async def lifespan(server: Server[Any] | MCPServer[Any]) -> AsyncIterator[None]: - lifecycle.append("start") - try: - yield None - finally: - lifecycle.append("stop") - - if highlevel: - server = MCPServer("Broker", lifespan=lifespan) - - @server.tool() - async def identify(ctx: Context) -> str: - return await identity(ctx.transport) - - else: - - async def list_tools(ctx: ServerRequestContext, params: PaginatedRequestParams | None) -> ListToolsResult: - return ListToolsResult(tools=[Tool(name="identify", input_schema={"type": "object"})]) - - async def call_tool(ctx: ServerRequestContext, params: CallToolRequestParams) -> CallToolResult: - assert params.name == "identify" - return CallToolResult(content=[TextContent(text=await identity(ctx.transport))]) - - server = Server("Broker", lifespan=lifespan, on_list_tools=list_tools, on_call_tool=call_tool) - - results: dict[str, str] = {} - - async def call(client: Client, principal: str) -> None: - result = await client.call_tool("identify") - content = result.content[0] - assert isinstance(content, TextContent) - results[principal] = content.text - - async with AsyncExitStack() as stack: - sessions = {principal: uuid4().hex for principal in entered} - transports = { - principal: await factory(stack, principal, session, True) for principal, session in sessions.items() - } - runtime = await stack.enter_async_context(server.serve()) - clients: dict[str, Client] = {} - for principal, transport in transports.items(): - await runtime.connect(transport, transport_builder=partial(peer_context, principal=principal, kind=kind)) - client_transport = await factory(stack, principal, sessions[principal], False) - clients[principal] = await stack.enter_async_context( - Client(client_transport, mode=mode, read_timeout_seconds=5) - ) - async with anyio.create_task_group() as tg: - for principal, client in clients.items(): - tg.start_soon(call, client, principal) - assert results == {"alice": "alice", "bob": "bob"} - assert lifecycle == ["start"] - assert lifecycle == ["start", "stop"] diff --git a/examples/transports/demo_grpc.py b/examples/transports/demo_grpc.py deleted file mode 100644 index ad14db06de..0000000000 --- a/examples/transports/demo_grpc.py +++ /dev/null @@ -1,71 +0,0 @@ -"""Exercise the native gRPC binding over a real loopback connection.""" - -from contextlib import AsyncExitStack - -import anyio -import grpc.aio -from mcp import Client -from mcp.server import Server, ServerRequestContext -from mcp.server.mcpserver import Context, MCPServer -from mcp.types import CallToolRequestParams, CallToolResult, ListToolsResult, PaginatedRequestParams, TextContent, Tool - -from mcp_transport_examples.grpc import grpc_client, grpc_server -from mcp_transport_examples.grpc_context import GRPCContext - - -async def verify(highlevel: bool, mode: str) -> None: - if highlevel: - server = MCPServer("Native gRPC") - - @server.tool() - async def echo(value: str, ctx: Context) -> str: - assert isinstance(ctx.transport, GRPCContext) - assert not ctx.transport.can_send_request - await ctx.report_progress(1, 2, "halfway") - return value - - else: - - async def list_tools(ctx: ServerRequestContext, params: PaginatedRequestParams | None) -> ListToolsResult: - return ListToolsResult(tools=[Tool(name="echo", input_schema={"type": "object"})]) - - async def call_tool(ctx: ServerRequestContext, params: CallToolRequestParams) -> CallToolResult: - assert params.name == "echo" - assert isinstance(ctx.transport, GRPCContext) - assert params.arguments is not None - await ctx.session.report_progress(1, 2, "halfway") - return CallToolResult(content=[TextContent(text=str(params.arguments["value"]))]) - - server = Server("Native gRPC", on_list_tools=list_tools, on_call_tool=call_tool) - - updates: list[tuple[float, float | None, str | None]] = [] - - async def progress(progress: float, total: float | None, message: str | None) -> None: - updates.append((progress, total, message)) - - async with AsyncExitStack() as stack: - listener = grpc.aio.server() - port = listener.add_insecure_port("127.0.0.1:0") - stack.push_async_callback(listener.stop, 0) - runtime = await stack.enter_async_context(server.serve()) - await runtime.connect(grpc_server(listener)) - await listener.start() - channel = await stack.enter_async_context(grpc.aio.insecure_channel(f"127.0.0.1:{port}")) - client = await stack.enter_async_context(Client(grpc_client(channel), mode=mode)) - value = "MCP without a JSON-RPC envelope" - result = await client.call_tool("echo", {"value": value}, progress_callback=progress) - content = result.content[0] - assert isinstance(content, TextContent) - assert content.text == value - assert updates == [(1, 2, "halfway")] - - -async def main() -> None: - for highlevel in (False, True): - for mode in ("auto", "2026-07-28"): - with anyio.fail_after(5): - await verify(highlevel, mode) - - -if __name__ == "__main__": - anyio.run(main) diff --git a/examples/transports/demo_grpc_features.py b/examples/transports/demo_grpc_features.py deleted file mode 100644 index 7418ecaaba..0000000000 --- a/examples/transports/demo_grpc_features.py +++ /dev/null @@ -1,91 +0,0 @@ -"""Live checks for subscriptions and multi-round-trip results over native gRPC.""" - -from contextlib import AsyncExitStack - -import anyio -import grpc.aio -from mcp import Client -from mcp.client import ClientRequestContext -from mcp.client.subscriptions import ToolsListChanged -from mcp.server.mcpserver import Context, MCPServer -from mcp.types import ElicitRequest, ElicitRequestFormParams, ElicitRequestParams, ElicitResult, InputRequiredResult - -from mcp_transport_examples.grpc import grpc_client, grpc_server - - -async def verify() -> None: - server = MCPServer("native features") - entered = {"alice": anyio.Event(), "bob": anyio.Event()} - - @server.tool() - async def overlap(label: str, ctx: Context) -> str: - assert ctx.request_context.request_id == 0 - entered[label].set() - await entered["bob" if label == "alice" else "alice"].wait() - return label - - @server.tool() - async def announce(ctx: Context) -> str: - await ctx.notify_tools_changed() - return "announced" - - @server.tool() - async def confirm(ctx: Context) -> str | InputRequiredResult: - if ctx.input_responses is not None: - assert ctx.request_state == "awaiting confirmation" - answer = ctx.input_responses["confirm"] - assert isinstance(answer, ElicitResult) - assert answer.action == "accept" - return "confirmed" - return InputRequiredResult( - input_requests={ - "confirm": ElicitRequest( - params=ElicitRequestFormParams( - message="Confirm?", requested_schema={"type": "object", "properties": {}} - ) - ) - }, - request_state="awaiting confirmation", - ) - - async def elicit(context: ClientRequestContext, params: ElicitRequestParams) -> ElicitResult: - return ElicitResult(action="accept", content={}) - - async with AsyncExitStack() as stack: - listener = grpc.aio.server() - port = listener.add_insecure_port("127.0.0.1:0") - stack.push_async_callback(listener.stop, 0) - runtime = await stack.enter_async_context(server.serve()) - await runtime.connect(grpc_server(listener)) - await listener.start() - channel = await stack.enter_async_context(grpc.aio.insecure_channel(f"127.0.0.1:{port}")) - client = await stack.enter_async_context(Client(grpc_client(channel), elicitation_callback=elicit)) - async with client.listen(tools_list_changed=True) as subscription: - result = await client.call_tool("announce") - assert result.structured_content == {"result": "announced"} - event = await anext(subscription) - assert isinstance(event, ToolsListChanged) - result = await client.call_tool("confirm") - assert result.structured_content == {"result": "confirmed"} - - clients: dict[str, Client] = {} - for label in entered: - peer_channel = await stack.enter_async_context(grpc.aio.insecure_channel(f"127.0.0.1:{port}")) - clients[label] = await stack.enter_async_context(Client(grpc_client(peer_channel), mode="2026-07-28")) - - async def call(label: str, client: Client) -> None: - result = await client.call_tool("overlap", {"label": label}) - assert result.structured_content == {"result": label} - - async with anyio.create_task_group() as tg: - for label, peer in clients.items(): - tg.start_soon(call, label, peer) - - -async def main() -> None: - with anyio.fail_after(5): - await verify() - - -if __name__ == "__main__": - anyio.run(main) diff --git a/examples/transports/demo_mqtt.py b/examples/transports/demo_mqtt.py deleted file mode 100644 index 55b1a96cee..0000000000 --- a/examples/transports/demo_mqtt.py +++ /dev/null @@ -1,39 +0,0 @@ -"""Exercise the MQTT 5 adapter against the local Mosquitto broker.""" - -import os -from contextlib import AsyncExitStack - -import aiomqtt -import anyio -from mcp.shared.transport import Transport - -from demo_common import verify -from mcp_transport_examples.mqtt import mqtt_transport - - -async def open_transport(stack: AsyncExitStack, principal: str, session: str, server_side: bool) -> Transport: - user = "server" if server_side else principal - client = await stack.enter_async_context( - aiomqtt.Client( - "127.0.0.1", - int(os.environ.get("MQTT_TEST_PORT", "13883")), - username=user, - password=f"test-{user}-password", - protocol=aiomqtt.ProtocolVersion.V5, - max_queued_incoming_messages=256, - ) - ) - topic = f"mcp/{principal}/{session}" - incoming, outgoing = ("requests", "responses") if server_side else ("responses", "requests") - return mqtt_transport(client, incoming_topic=f"{topic}/{incoming}", outgoing_topic=f"{topic}/{outgoing}") - - -async def main() -> None: - for highlevel in (False, True): - for mode in ("legacy", "auto", "2026-07-28"): - with anyio.fail_after(5): - await verify(open_transport, kind="mqtt", highlevel=highlevel, mode=mode) - - -if __name__ == "__main__": - anyio.run(main) diff --git a/examples/transports/mcp_transport_examples/__init__.py b/examples/transports/mcp_transport_examples/__init__.py deleted file mode 100644 index a9a2c5b3bb..0000000000 --- a/examples/transports/mcp_transport_examples/__init__.py +++ /dev/null @@ -1 +0,0 @@ -__all__ = [] diff --git a/examples/transports/mcp_transport_examples/_grpc_codec.py b/examples/transports/mcp_transport_examples/_grpc_codec.py deleted file mode 100644 index 63b8fa47ca..0000000000 --- a/examples/transports/mcp_transport_examples/_grpc_codec.py +++ /dev/null @@ -1,35 +0,0 @@ -"""JSON payloads inside the experimental protobuf binding.""" - -from __future__ import annotations - -import json -from typing import Any, NoReturn, cast - -MAX_PAYLOAD_SIZE = 4 * 1024 * 1024 -RPC_METHOD = "/mcp.transport.example.MCP/Call" - - -def encode_json(value: Any) -> bytes: - payload = json.dumps(value, allow_nan=False, ensure_ascii=True, separators=(",", ":")).encode("utf-8") - if len(payload) > MAX_PAYLOAD_SIZE: - raise ValueError("Payload exceeds the gRPC binding's size limit") - return payload - - -def decode_json(payload: bytes) -> Any: - if len(payload) > MAX_PAYLOAD_SIZE: - raise ValueError("Payload exceeds the gRPC binding's size limit") - return json.loads(payload, parse_constant=reject_constant) - - -def decode_object(payload: bytes, *, nullable: bool = False) -> dict[str, Any] | None: - value = decode_json(payload) - if isinstance(value, dict): - return cast("dict[str, Any]", value) - if nullable and value is None: - return None - raise ValueError("Expected a JSON object") - - -def reject_constant(value: str) -> NoReturn: - raise ValueError(f"Invalid JSON constant: {value}") diff --git a/examples/transports/mcp_transport_examples/amqp.py b/examples/transports/mcp_transport_examples/amqp.py deleted file mode 100644 index 98573d8b01..0000000000 --- a/examples/transports/mcp_transport_examples/amqp.py +++ /dev/null @@ -1,138 +0,0 @@ -"""A symmetric AMQP 0.9.1 transport for one logical MCP peer.""" - -from __future__ import annotations - -from collections.abc import AsyncIterator -from contextlib import asynccontextmanager, suppress -from dataclasses import dataclass -from types import TracebackType - -import anyio -from aio_pika import Message -from aio_pika.abc import AbstractChannel, AbstractIncomingMessage -from aio_pika.exceptions import AMQPError, ChannelInvalidStateError -from mcp.shared.transport import SessionMessage, TransportStreams -from mcp.types import jsonrpc_message_adapter -from pamqp.common import Arguments -from pydantic import ValidationError -from typing_extensions import Self - - -@asynccontextmanager -async def amqp_transport( - channel: AbstractChannel, - *, - incoming_queue: str, - outgoing_queue: str, - expiry: int = 60, - max_message_size: int = 4 * 1024 * 1024, -) -> AsyncIterator[TransportStreams]: - """Connect a peer over two dedicated queues without automatic replay. - - You own `channel` and its connection. Use a fresh queue pair for every - logical connection and restrict queue access with broker permissions. - Messages are acknowledged before SDK handoff; redeliveries are rejected. - This avoids automatically repeating side effects but can lose work after - acknowledgment. A publisher confirmation is not tool completion. - - Raises: - ValueError: If routing, limits, or publisher-confirm settings are invalid. - AMQPError: If queue setup or publication fails. - """ - if not incoming_queue or not outgoing_queue or incoming_queue == outgoing_queue: - raise ValueError("AMQP directions must use different nonempty queue names") - if expiry < 1 or max_message_size < 1 or not channel.publisher_confirms: - raise ValueError("Positive limits and publisher confirmations are required") - arguments: Arguments = {"x-expires": expiry * 1000, "x-max-length": 256, "x-overflow": "reject-publish"} - incoming = await channel.declare_queue(incoming_queue, auto_delete=True, arguments=arguments) - await channel.declare_queue(outgoing_queue, auto_delete=True, arguments=arguments) - await channel.set_qos(prefetch_count=16) - send, receive = anyio.create_memory_object_stream[SessionMessage | Exception](0) - writer = _AMQPWriter(channel, outgoing_queue, expiry, max_message_size) - - lock = anyio.Lock() - active: set[anyio.Event] = set() - - def channel_closed(sender: object, exc: BaseException | None) -> None: - send.close() - - async def deliver(message: AbstractIncomingMessage) -> None: - finished = anyio.Event() - active.add(finished) - try: - async with lock: - if message.redelivered: - await message.reject(requeue=False) - await send.send(ValueError("AMQP redelivery is not replayed")) - return - await message.ack() - if len(message.body) > max_message_size or message.content_type != "application/json": - await send.send(ValueError("Rejected oversized or non-JSON AMQP message")) - return - if not message.body: - send.close() - return - try: - decoded = jsonrpc_message_adapter.validate_json(message.body, by_name=False) - except ValidationError as exc: - await send.send(exc) - else: - await send.send(SessionMessage(decoded)) - except (AMQPError, ChannelInvalidStateError): - send.close() - except (anyio.BrokenResourceError, anyio.ClosedResourceError): - pass - finally: - active.remove(finished) - finished.set() - - async with send, receive: - channel.close_callbacks.add(channel_closed) - try: - tag = await incoming.consume(deliver, exclusive=True) - try: - async with writer: - yield receive, writer - finally: - send.close() - with anyio.move_on_after(1, shield=True), suppress(AMQPError, ChannelInvalidStateError): - await incoming.cancel(tag) - for finished in tuple(active): - await finished.wait() - finally: - channel.close_callbacks.discard(channel_closed) - - -@dataclass -class _AMQPWriter: - channel: AbstractChannel - queue: str - expiry: int - max_message_size: int - closed: bool = False - - async def send(self, item: SessionMessage, /) -> None: - if self.closed: - raise anyio.ClosedResourceError - payload = item.message.model_dump_json(by_alias=True, exclude_none=True).encode() - if len(payload) > self.max_message_size: - raise ValueError("Encoded MCP message exceeds max_message_size") - await self.channel.default_exchange.publish( - Message(payload, content_type="application/json", expiration=self.expiry), routing_key=self.queue - ) - - async def aclose(self) -> None: - if not self.closed: - self.closed = True - with anyio.move_on_after(1, shield=True), suppress(AMQPError, ChannelInvalidStateError): - await self.channel.default_exchange.publish( - Message(b"", content_type="application/json", expiration=self.expiry), routing_key=self.queue - ) - - async def __aenter__(self) -> Self: - return self - - async def __aexit__( - self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None - ) -> None: - await self.aclose() diff --git a/examples/transports/mcp_transport_examples/grpc.py b/examples/transports/mcp_transport_examples/grpc.py deleted file mode 100644 index 4f353a1af2..0000000000 --- a/examples/transports/mcp_transport_examples/grpc.py +++ /dev/null @@ -1,38 +0,0 @@ -"""Factories for the experimental native protobuf transport.""" - -from __future__ import annotations - -from collections.abc import AsyncIterator -from contextlib import asynccontextmanager - -import grpc.aio -from mcp.shared.dispatcher import Dispatcher -from mcp.shared.transport import DispatcherTransport, TransportContext - -from mcp_transport_examples.grpc_client import GRPCClientDispatcher -from mcp_transport_examples.grpc_server import GRPCServerDispatcher - - -def grpc_client(channel: grpc.aio.Channel) -> DispatcherTransport: - """Use a borrowed channel with `Client`; the caller owns TLS, credentials, and channel closure.""" - - @asynccontextmanager - async def connection() -> AsyncIterator[Dispatcher[TransportContext]]: - yield GRPCClientDispatcher(channel) - - return DispatcherTransport(connection()) - - -def grpc_server(server: grpc.aio.Server, *, max_requests: int = 64) -> DispatcherTransport: - """Attach one MCP binding to a borrowed server before starting its listener. - - Connect this transport to `ServerRuntime`, then start the gRPC server. - The caller owns the listener and other registered gRPC services. Runtime - shutdown cancels MCP handlers without stopping unrelated services. - """ - - @asynccontextmanager - async def connection() -> AsyncIterator[Dispatcher[TransportContext]]: - yield GRPCServerDispatcher(server, max_requests=max_requests) - - return DispatcherTransport(connection()) diff --git a/examples/transports/mcp_transport_examples/grpc_client.py b/examples/transports/mcp_transport_examples/grpc_client.py deleted file mode 100644 index 9d804334a8..0000000000 --- a/examples/transports/mcp_transport_examples/grpc_client.py +++ /dev/null @@ -1,139 +0,0 @@ -"""A native gRPC dispatcher that reuses the SDK's high-level client.""" - -from __future__ import annotations - -import asyncio -from collections.abc import Mapping -from typing import Any - -import anyio -import anyio.abc -import grpc -import grpc.aio -from mcp.shared.dispatcher import ( - CallOptions, - OnNotify, - OnNotifyIntercept, - OnRequest, - coerce_request_id, -) -from mcp.shared.exceptions import MCPError, NoBackChannelError -from mcp.types import CONNECTION_CLOSED, REQUEST_TIMEOUT, ErrorData, RequestId - -from mcp_transport_examples._grpc_codec import RPC_METHOD, decode_object, encode_json -from mcp_transport_examples.grpc_context import GRPCContext, GRPCDispatchContext -from mcp_transport_examples.grpc_response import PendingCall, receive_response -from mcp_transport_examples.rpc_pb2 import CallEvent, CallRequest - - -class GRPCClientDispatcher: - """Run MCP calls on a borrowed gRPC channel, with one response stream per request.""" - - def __init__(self, channel: grpc.aio.Channel) -> None: - self._channel = channel - self._rpc = channel.unary_stream( - RPC_METHOD, request_serializer=CallRequest.SerializeToString, response_deserializer=CallEvent.FromString - ) - self._on_notify: OnNotify | None = None - self._intercept: OnNotifyIntercept | None = None - self._calls: dict[RequestId, PendingCall] = {} - self._next_id = 0 - self._closed = False - - async def run( - self, - on_request: OnRequest, - on_notify: OnNotify, - on_notify_intercept: OnNotifyIntercept | None = None, - *, - task_status: anyio.abc.TaskStatus[None] = anyio.TASK_STATUS_IGNORED, - ) -> None: - """Enable requests and cancel active RPCs when the client session exits.""" - self._on_notify = on_notify - self._intercept = on_notify_intercept - task_status.started() - try: - state = self._channel.get_state() - while state != grpc.ChannelConnectivity.SHUTDOWN: - await self._channel.wait_for_state_change(state) - state = self._channel.get_state() - finally: - self._closed = True - self._on_notify = None - pending = tuple(self._calls.values()) - for request in pending: - request.scope.cancel() - request.call.cancel() - with anyio.CancelScope(shield=True): - for request in pending: - await request.done.wait() - - async def send_raw_request( - self, method: str, params: Mapping[str, Any] | None, opts: CallOptions | None = None - ) -> dict[str, Any]: - """Send a native RPC and route its notifications before returning the final result. - - Raises: - MCPError: A peer error, request timeout, or closed connection. - """ - if self._closed or self._channel.get_state() == grpc.ChannelConnectivity.SHUTDOWN: - raise MCPError(code=CONNECTION_CLOSED, message="Connection closed") - on_notify = self._on_notify - if on_notify is None: - raise RuntimeError("GRPCClientDispatcher.run() has not started") - opts = opts or {} - request_id = opts.get("request_id") - if request_id is None: - while self._next_id in self._calls: - self._next_id += 1 - request_id = self._next_id - self._next_id += 1 - key = coerce_request_id(request_id) - if key in self._calls: - raise ValueError(f"Request id {request_id!r} is already in flight") - request = CallRequest( - method=method, - params_json=encode_json(params), - request_id_json=encode_json(request_id), - report_progress="on_progress" in opts, - ) - call = self._rpc(request, timeout=opts.get("timeout")) - pending = PendingCall(call) - self._calls[key] = pending - complete = False - terminal: CallEvent | None = None - dctx = GRPCDispatchContext(GRPCContext(kind="grpc", can_send_request=False, peer="server"), None, self.notify) - try: - with pending.scope, anyio.fail_after(opts.get("timeout")): - terminal = await receive_response(call, dctx, opts, on_notify, self._intercept) - complete = True - if terminal is None: - raise MCPError(code=CONNECTION_CLOSED, message="Connection closed") - if terminal.WhichOneof("payload") == "error_json": - raise MCPError.from_error_data(ErrorData.model_validate(decode_object(terminal.error_json))) - result = decode_object(terminal.result_json) - assert result is not None - return result - except grpc.aio.AioRpcError as exc: - code = REQUEST_TIMEOUT if exc.code() == grpc.StatusCode.DEADLINE_EXCEEDED else CONNECTION_CLOSED - raise MCPError( - code=code, message="gRPC request timed out" if code == REQUEST_TIMEOUT else "gRPC connection failed" - ) from exc - except ValueError as exc: - raise MCPError(code=CONNECTION_CLOSED, message="Invalid gRPC response") from exc - except TimeoutError as exc: - raise MCPError(code=REQUEST_TIMEOUT, message="gRPC request timed out") from exc - except asyncio.CancelledError: - if self._closed or self._channel.get_state() == grpc.ChannelConnectivity.SHUTDOWN: - raise MCPError(code=CONNECTION_CLOSED, message="Connection closed") from None - raise - finally: - self._calls.pop(key) - if not complete: - call.cancel() - pending.done.set() - - async def notify(self, method: str, params: Mapping[str, Any] | None, opts: CallOptions | None = None) -> None: - """The modern native binding uses structural cancellation, not client notifications.""" - if not self._closed: - raise NoBackChannelError(method) diff --git a/examples/transports/mcp_transport_examples/grpc_context.py b/examples/transports/mcp_transport_examples/grpc_context.py deleted file mode 100644 index 7f93be868a..0000000000 --- a/examples/transports/mcp_transport_examples/grpc_context.py +++ /dev/null @@ -1,64 +0,0 @@ -"""Request-scoped metadata and notifications for the native gRPC binding.""" - -from __future__ import annotations - -from collections.abc import Awaitable, Callable, Mapping -from dataclasses import dataclass, field -from typing import Any - -import anyio -from mcp.shared.dispatcher import CallOptions -from mcp.shared.exceptions import NoBackChannelError -from mcp.shared.transport import MessageMetadata, TransportContext -from mcp.types import RequestId - - -@dataclass(kw_only=True, frozen=True) -class GRPCContext(TransportContext): - """gRPC peer information, including identities verified by the configured transport. - - Invocation metadata is untrusted. Peer identities are empty on insecure - connections; the application decides which verified identities to authorize. - """ - - peer: str - metadata: tuple[tuple[str, str | bytes], ...] = () - peer_identity_key: str | None = None - peer_identities: tuple[bytes, ...] = () - - -@dataclass -class GRPCDispatchContext: - """Notifications are scoped to one RPC; server-initiated requests are unavailable.""" - - transport: GRPCContext - request_id: RequestId | None - send_notification: Callable[[str, Mapping[str, Any] | None], Awaitable[None]] - report_progress: bool = False - message_metadata: MessageMetadata = None - cancel_requested: anyio.Event = field(default_factory=anyio.Event) - - @property - def can_send_request(self) -> bool: - """The modern binding has no server-initiated request channel.""" - return False - - async def send_raw_request( - self, method: str, params: Mapping[str, Any] | None, opts: CallOptions | None = None - ) -> dict[str, Any]: - """Reject requests on this request-scoped channel.""" - raise NoBackChannelError(method) - - async def notify(self, method: str, params: Mapping[str, Any] | None, opts: CallOptions | None = None) -> None: - """Deliver a notification on the originating RPC.""" - await self.send_notification(method, params) - - async def progress(self, progress: float, total: float | None = None, message: str | None = None) -> None: - """Send progress only when the caller requested it.""" - if self.report_progress: - params: dict[str, Any] = {"progressToken": self.request_id, "progress": progress} - if total is not None: - params["total"] = total - if message is not None: - params["message"] = message - await self.notify("notifications/progress", params) diff --git a/examples/transports/mcp_transport_examples/grpc_response.py b/examples/transports/mcp_transport_examples/grpc_response.py deleted file mode 100644 index c72bbe1fd6..0000000000 --- a/examples/transports/mcp_transport_examples/grpc_response.py +++ /dev/null @@ -1,58 +0,0 @@ -"""Consume native response streams and deliver request-scoped notifications.""" - -from __future__ import annotations - -import logging -from collections.abc import AsyncIterable -from dataclasses import dataclass, field - -import anyio -import grpc.aio -from mcp.shared.dispatcher import CallOptions, OnNotify, OnNotifyIntercept, run_notify_intercept -from mcp.types import ProgressNotificationParams - -from mcp_transport_examples._grpc_codec import decode_object -from mcp_transport_examples.grpc_context import GRPCDispatchContext -from mcp_transport_examples.rpc_pb2 import CallEvent, CallRequest - -logger = logging.getLogger(__name__) - - -@dataclass(frozen=True) -class PendingCall: - """Track both the native RPC and callbacks executing in its caller's task.""" - - call: grpc.aio.UnaryStreamCall[CallRequest, CallEvent] - scope: anyio.CancelScope = field(default_factory=anyio.CancelScope) - done: anyio.Event = field(default_factory=anyio.Event) - - -async def receive_response( - events: AsyncIterable[CallEvent], - context: GRPCDispatchContext, - opts: CallOptions, - on_notify: OnNotify, - intercept: OnNotifyIntercept | None, -) -> CallEvent: - """Deliver notifications in receive order, then require exactly one terminal event followed by EOF.""" - terminal: CallEvent | None = None - async for event in events: - kind = event.WhichOneof("payload") - if terminal is not None or kind is None: - raise ValueError("Invalid gRPC response sequence") - if kind != "notification": - terminal = event - continue - notification = event.notification - data = decode_object(notification.params_json, nullable=True) - if notification.method == "notifications/progress" and "on_progress" in opts: - progress = ProgressNotificationParams.model_validate(data, by_name=False) - try: - await opts["on_progress"](progress.progress, progress.total, progress.message) - except Exception: - logger.exception("Progress callback failed") - if not run_notify_intercept(intercept, notification.method, data): - await on_notify(context, notification.method, data) - if terminal is None: - raise ValueError("gRPC call ended without an MCP result") - return terminal diff --git a/examples/transports/mcp_transport_examples/grpc_server.py b/examples/transports/mcp_transport_examples/grpc_server.py deleted file mode 100644 index 58933972ae..0000000000 --- a/examples/transports/mcp_transport_examples/grpc_server.py +++ /dev/null @@ -1,158 +0,0 @@ -"""Serve native protobuf RPCs through the SDK's dispatcher interface.""" - -from __future__ import annotations - -import asyncio -from collections.abc import Mapping -from typing import Any, cast - -import anyio -import anyio.abc -import grpc -import grpc.aio -from mcp.shared.dispatcher import CallOptions, OnNotify, OnNotifyIntercept, OnRequest, as_request_id -from mcp.shared.exceptions import MCPError, NoBackChannelError -from mcp.types import INVALID_PARAMS -from pydantic import ValidationError - -from mcp_transport_examples._grpc_codec import decode_json, decode_object, encode_json -from mcp_transport_examples.grpc_context import GRPCContext, GRPCDispatchContext -from mcp_transport_examples.rpc_pb2 import CallEvent, CallRequest, Notification - - -class GRPCServerDispatcher: - """Attach MCP to a borrowed gRPC server without taking ownership of its listener. - - Register before starting the server. The runtime starts this dispatcher; - you start and stop the gRPC server. Each RPC has independent MCP metadata - and a response stream. Shutdown cancels and joins active request handlers. - """ - - def __init__(self, server: grpc.aio.Server, *, max_requests: int = 64) -> None: - if max_requests < 1: - raise ValueError("max_requests must be positive") - self._limit = anyio.CapacityLimiter(max_requests) - self._handler: OnRequest | None = None - self._requests: dict[anyio.CancelScope, anyio.Event] = {} - self._stopped = anyio.Event() - handler = grpc.unary_stream_rpc_method_handler( - self.handle, request_deserializer=CallRequest.FromString, response_serializer=CallEvent.SerializeToString - ) - server.add_generic_rpc_handlers( - [grpc.method_handlers_generic_handler("mcp.transport.example.MCP", {"Call": handler})] - ) - - async def run( - self, - on_request: OnRequest, - on_notify: OnNotify, - on_notify_intercept: OnNotifyIntercept | None = None, - *, - task_status: anyio.abc.TaskStatus[None] = anyio.TASK_STATUS_IGNORED, - ) -> None: - """Install the MCP handler and wait until runtime shutdown.""" - self._handler = on_request - task_status.started() - try: - await self._stopped.wait() - finally: - self._handler = None - self._stopped.set() - requests = tuple(self._requests.items()) - for scope, _ in requests: - scope.cancel() - with anyio.CancelScope(shield=True): - for _, done in requests: - await done.wait() - - async def handle(self, request: CallRequest, context: grpc.aio.ServicerContext[CallRequest, CallEvent]) -> None: - """Handle one gRPC call, with native cancellation and request-scoped notification delivery.""" - handler = self._handler - if handler is None: - await context.abort(grpc.StatusCode.UNAVAILABLE, "MCP dispatcher is not running") - try: - self._limit.acquire_nowait() - except anyio.WouldBlock: - await context.abort(grpc.StatusCode.RESOURCE_EXHAUSTED, "MCP request capacity exhausted") - scope = anyio.CancelScope() - done = anyio.Event() - self._requests[scope] = done - lock = anyio.Lock() - - async def notify(method: str, params: Mapping[str, Any] | None) -> None: - async with lock: - await context.write( - CallEvent(notification=Notification(method=method, params_json=encode_json(params))) - ) - - try: - try: - params = decode_object(request.params_json, nullable=True) - request_id = as_request_id(decode_json(request.request_id_json)) - if request_id is None: - raise ValueError("Invalid request id") - except (ValueError, UnicodeError): - await context.abort(grpc.StatusCode.INVALID_ARGUMENT, "Invalid MCP binding payload") - dctx = GRPCDispatchContext( - transport=GRPCContext( - kind="grpc", - can_send_request=False, - peer=context.peer(), - metadata=cast("tuple[tuple[str, str | bytes], ...]", tuple(context.invocation_metadata() or ())), - peer_identity_key=context.peer_identity_key(), - peer_identities=tuple(context.peer_identities() or ()), - ), - request_id=request_id, - send_notification=notify, - report_progress=request.report_progress, - ) - - response: CallEvent | None = None - ready = anyio.Event() - - async def invoke() -> None: - nonlocal response - try: - result = await handler(dctx, request.method, params) - except MCPError as exc: - response = CallEvent(error_json=encode_json(exc.error.model_dump(by_alias=True))) - except ValidationError: - response = CallEvent( - error_json=encode_json( - {"code": INVALID_PARAMS, "message": "Invalid request parameters", "data": ""} - ) - ) - else: - response = CallEvent(result_json=encode_json(result)) - finally: - ready.set() - - with scope: - async with anyio.create_task_group() as tg: - tg.start_soon(invoke) - try: - await ready.wait() - except asyncio.CancelledError: - if not scope.cancel_called and not tg.cancel_scope.cancel_called: - dctx.cancel_requested.set() - raise - if response is None: - await context.abort(grpc.StatusCode.CANCELLED, "MCP handler ended without a result") - async with lock: - await context.write(response) - if scope.cancelled_caught: - await context.abort(grpc.StatusCode.UNAVAILABLE, "MCP dispatcher closed") - finally: - self._requests.pop(scope) - done.set() - self._limit.release() - - async def send_raw_request( - self, method: str, params: Mapping[str, Any] | None, opts: CallOptions | None = None - ) -> dict[str, Any]: - """Reject server-initiated requests in the modern binding.""" - raise NoBackChannelError(method) - - async def notify(self, method: str, params: Mapping[str, Any] | None, opts: CallOptions | None = None) -> None: - """Reject notifications without an originating RPC; use its DispatchContext instead.""" - raise NoBackChannelError(method) diff --git a/examples/transports/mcp_transport_examples/mqtt.py b/examples/transports/mcp_transport_examples/mqtt.py deleted file mode 100644 index bb6bc511e1..0000000000 --- a/examples/transports/mcp_transport_examples/mqtt.py +++ /dev/null @@ -1,125 +0,0 @@ -"""A symmetric MQTT 5 transport for one logical MCP peer.""" - -from __future__ import annotations - -from collections.abc import AsyncIterator -from contextlib import asynccontextmanager, suppress -from dataclasses import dataclass -from types import TracebackType - -import aiomqtt -import anyio -from mcp.shared.transport import SessionMessage, TransportStreams -from mcp.types import jsonrpc_message_adapter -from paho.mqtt.packettypes import PacketTypes -from paho.mqtt.properties import Properties -from paho.mqtt.subscribeoptions import SubscribeOptions -from pydantic import ValidationError -from typing_extensions import Self - - -@asynccontextmanager -async def mqtt_transport( - client: aiomqtt.Client, - *, - incoming_topic: str, - outgoing_topic: str, - expiry: int = 60, - max_message_size: int = 4 * 1024 * 1024, -) -> AsyncIterator[TransportStreams]: - """Connect one peer over two dedicated MQTT 5 topics using QoS 2. - - You own and enter `client`. Give each connection fresh topics, grant only - its peer access through broker ACLs, and dedicate the client's messages - iterator to this transport. Empty payloads close the logical connection. - Retained messages are rejected; this adapter never reconnects or replays. - - Args: - client: An entered MQTT 5 client with a bounded incoming queue. - incoming_topic: Exact topic to receive from, without wildcards. - outgoing_topic: Exact topic to publish to, without wildcards. - expiry: Broker expiry for messages and the close signal, in seconds. - max_message_size: Maximum encoded message size in either direction. - - Raises: - ValueError: If the configuration is invalid or an outgoing message is too large. - aiomqtt.MqttError: If subscription or publication fails. - """ - aiomqtt.Topic(incoming_topic) - aiomqtt.Topic(outgoing_topic) - if incoming_topic == outgoing_topic: - raise ValueError("MQTT directions must use different topics") - if not 0 < expiry <= 2**32 - 1 or max_message_size < 1: - raise ValueError("expiry must be a positive uint32 and max_message_size must be positive") - properties = Properties(PacketTypes.PUBLISH) - properties.MessageExpiryInterval = expiry - writer = _MQTTWriter(client, outgoing_topic, properties, max_message_size) - send, receive = anyio.create_memory_object_stream[SessionMessage | Exception](0) - - async def read_messages() -> None: - async with send: - try: - async for message in client.messages: - if str(message.topic) != incoming_topic: - continue - if message.retain or message.qos != 2 or len(message.payload) > max_message_size: - await send.send(ValueError("Rejected retained, non-QoS-2, or oversized MQTT message")) - continue - if not message.payload: - break - try: - decoded = jsonrpc_message_adapter.validate_json(message.payload, by_name=False) - except ValidationError as exc: - await send.send(exc) - else: - await send.send(SessionMessage(decoded)) - except (aiomqtt.MqttError, anyio.BrokenResourceError, anyio.ClosedResourceError): - pass - - try: - await client.subscribe( - incoming_topic, options=SubscribeOptions(qos=2, retainAsPublished=True, retainHandling=2) - ) - async with receive, writer: - async with anyio.create_task_group() as tg: - tg.start_soon(read_messages) - try: - yield receive, writer - finally: - tg.cancel_scope.cancel() - finally: - await send.aclose() - await receive.aclose() - with anyio.move_on_after(1, shield=True), suppress(aiomqtt.MqttError): - await client.unsubscribe(incoming_topic) - - -@dataclass -class _MQTTWriter: - client: aiomqtt.Client - topic: str - properties: Properties - max_message_size: int - closed: bool = False - - async def send(self, item: SessionMessage, /) -> None: - if self.closed: - raise anyio.ClosedResourceError - payload = item.message.model_dump_json(by_alias=True, exclude_none=True).encode() - if len(payload) > self.max_message_size: - raise ValueError("Encoded MCP message exceeds max_message_size") - await self.client.publish(self.topic, payload, qos=2, retain=False, properties=self.properties) - - async def aclose(self) -> None: - if not self.closed: - self.closed = True - with anyio.move_on_after(1, shield=True), suppress(aiomqtt.MqttError): - await self.client.publish(self.topic, b"", qos=2, retain=False, properties=self.properties) - - async def __aenter__(self) -> Self: - return self - - async def __aexit__( - self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None - ) -> None: - await self.aclose() diff --git a/examples/transports/mcp_transport_examples/py.typed b/examples/transports/mcp_transport_examples/py.typed deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/examples/transports/mcp_transport_examples/rpc.proto b/examples/transports/mcp_transport_examples/rpc.proto deleted file mode 100644 index 9f660545f0..0000000000 --- a/examples/transports/mcp_transport_examples/rpc.proto +++ /dev/null @@ -1,27 +0,0 @@ -syntax = "proto3"; - -package mcp.transport.example; - -service MCP { - rpc Call(CallRequest) returns (stream CallEvent); -} - -message CallRequest { - string method = 1; - bytes params_json = 2; - bytes request_id_json = 3; - bool report_progress = 4; -} - -message CallEvent { - oneof payload { - bytes result_json = 1; - bytes error_json = 2; - Notification notification = 3; - } -} - -message Notification { - string method = 1; - bytes params_json = 2; -} diff --git a/examples/transports/mcp_transport_examples/rpc_pb2.py b/examples/transports/mcp_transport_examples/rpc_pb2.py deleted file mode 100644 index c2d2b563a5..0000000000 --- a/examples/transports/mcp_transport_examples/rpc_pb2.py +++ /dev/null @@ -1,42 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by the protocol buffer compiler. DO NOT EDIT! -# NO CHECKED-IN PROTOBUF GENCODE -# source: mcp_transport_examples/rpc.proto -# Protobuf Python Version: 6.33.5 -"""Generated protocol buffer code.""" -from google.protobuf import descriptor as _descriptor -from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import runtime_version as _runtime_version -from google.protobuf import symbol_database as _symbol_database -from google.protobuf.internal import builder as _builder -_runtime_version.ValidateProtobufRuntimeVersion( - _runtime_version.Domain.PUBLIC, - 6, - 33, - 5, - '', - 'mcp_transport_examples/rpc.proto' -) -# @@protoc_insertion_point(imports) - -_sym_db = _symbol_database.Default() - - - - -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n mcp_transport_examples/rpc.proto\x12\x15mcp.transport.example\"d\n\x0b\x43\x61llRequest\x12\x0e\n\x06method\x18\x01 \x01(\t\x12\x13\n\x0bparams_json\x18\x02 \x01(\x0c\x12\x17\n\x0frequest_id_json\x18\x03 \x01(\x0c\x12\x17\n\x0freport_progress\x18\x04 \x01(\x08\"\x80\x01\n\tCallEvent\x12\x15\n\x0bresult_json\x18\x01 \x01(\x0cH\x00\x12\x14\n\nerror_json\x18\x02 \x01(\x0cH\x00\x12;\n\x0cnotification\x18\x03 \x01(\x0b\x32#.mcp.transport.example.NotificationH\x00\x42\t\n\x07payload\"3\n\x0cNotification\x12\x0e\n\x06method\x18\x01 \x01(\t\x12\x13\n\x0bparams_json\x18\x02 \x01(\x0c\x32U\n\x03MCP\x12N\n\x04\x43\x61ll\x12\".mcp.transport.example.CallRequest\x1a .mcp.transport.example.CallEvent0\x01\x62\x06proto3') - -_globals = globals() -_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'mcp_transport_examples.rpc_pb2', _globals) -if not _descriptor._USE_C_DESCRIPTORS: - DESCRIPTOR._loaded_options = None - _globals['_CALLREQUEST']._serialized_start=59 - _globals['_CALLREQUEST']._serialized_end=159 - _globals['_CALLEVENT']._serialized_start=162 - _globals['_CALLEVENT']._serialized_end=290 - _globals['_NOTIFICATION']._serialized_start=292 - _globals['_NOTIFICATION']._serialized_end=343 - _globals['_MCP']._serialized_start=345 - _globals['_MCP']._serialized_end=430 -# @@protoc_insertion_point(module_scope) diff --git a/examples/transports/mcp_transport_examples/rpc_pb2.pyi b/examples/transports/mcp_transport_examples/rpc_pb2.pyi deleted file mode 100644 index cc41d17add..0000000000 --- a/examples/transports/mcp_transport_examples/rpc_pb2.pyi +++ /dev/null @@ -1,36 +0,0 @@ -from google.protobuf import descriptor as _descriptor -from google.protobuf import message as _message -from collections.abc import Mapping as _Mapping -from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union - -DESCRIPTOR: _descriptor.FileDescriptor - -class CallRequest(_message.Message): - __slots__ = ("method", "params_json", "request_id_json", "report_progress") - METHOD_FIELD_NUMBER: _ClassVar[int] - PARAMS_JSON_FIELD_NUMBER: _ClassVar[int] - REQUEST_ID_JSON_FIELD_NUMBER: _ClassVar[int] - REPORT_PROGRESS_FIELD_NUMBER: _ClassVar[int] - method: str - params_json: bytes - request_id_json: bytes - report_progress: bool - def __init__(self, method: _Optional[str] = ..., params_json: _Optional[bytes] = ..., request_id_json: _Optional[bytes] = ..., report_progress: _Optional[bool] = ...) -> None: ... - -class CallEvent(_message.Message): - __slots__ = ("result_json", "error_json", "notification") - RESULT_JSON_FIELD_NUMBER: _ClassVar[int] - ERROR_JSON_FIELD_NUMBER: _ClassVar[int] - NOTIFICATION_FIELD_NUMBER: _ClassVar[int] - result_json: bytes - error_json: bytes - notification: Notification - def __init__(self, result_json: _Optional[bytes] = ..., error_json: _Optional[bytes] = ..., notification: _Optional[_Union[Notification, _Mapping]] = ...) -> None: ... - -class Notification(_message.Message): - __slots__ = ("method", "params_json") - METHOD_FIELD_NUMBER: _ClassVar[int] - PARAMS_JSON_FIELD_NUMBER: _ClassVar[int] - method: str - params_json: bytes - def __init__(self, method: _Optional[str] = ..., params_json: _Optional[bytes] = ...) -> None: ... diff --git a/examples/transports/pyproject.toml b/examples/transports/pyproject.toml deleted file mode 100644 index 6211c54352..0000000000 --- a/examples/transports/pyproject.toml +++ /dev/null @@ -1,68 +0,0 @@ -[project] -name = "mcp-transport-examples" -version = "0.1.0" -description = "Reference MQTT, AMQP, and native gRPC adapters for the MCP transport API" -requires-python = ">=3.10" -dependencies = [ - "aio-pika>=9.5", - "aiomqtt>=2.4", - "grpcio>=1.71", - "mcp", - "protobuf>=6.33.5", -] - -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" - -[tool.hatch.build.targets.wheel] -packages = ["mcp_transport_examples"] - -[dependency-groups] -dev = [ - "pytest>=8.4.0", - "coverage[toml]>=7.10.7", - "pyright>=1.1.400", - "ruff>=0.8.5", - "grpcio-tools==1.81.1", - "types-protobuf>=7.35.1.20260906", - "cassetter[grpc]>=0.11.0", - "cryptography>=50.0.0", -] - -[tool.pytest.ini_options] -addopts = "--strict-config --strict-markers" -testpaths = ["tests"] -filterwarnings = ["error"] -xfail_strict = true - -[tool.coverage.run] -branch = true -source_pkgs = ["mcp_transport_examples", "tests"] -# Protoc output is verified by regenerating it, not by testing protobuf internals. -omit = ["*/rpc_pb2.py"] - -[tool.coverage.report] -fail_under = 100 -show_missing = true -exclude_also = ["if TYPE_CHECKING:", "raise NotImplementedError", "@overload"] - -[tool.pyright] -typeCheckingMode = "strict" -include = ["mcp_transport_examples", "tests", "*.py"] -# Protoc emits unparameterized Mapping annotations in its generated stubs. -ignore = ["mcp_transport_examples/rpc_pb2.py", "mcp_transport_examples/rpc_pb2.pyi"] -venvPath = "." -venv = ".venv" -reportUnusedFunction = false - -[tool.ruff] -line-length = 120 -target-version = "py310" -extend-exclude = ["rpc_pb2.py", "rpc_pb2.pyi"] - -[tool.ruff.lint] -select = ["E", "F", "I", "FA", "UP", "RUF100"] - -[tool.ruff.lint.isort] -combine-as-imports = true diff --git a/examples/transports/reproduce_grpc_loop_shutdown.py b/examples/transports/reproduce_grpc_loop_shutdown.py deleted file mode 100644 index 2307e97e18..0000000000 --- a/examples/transports/reproduce_grpc_loop_shutdown.py +++ /dev/null @@ -1,37 +0,0 @@ -"""Reproduce late gRPC connectivity completions without importing the MCP SDK.""" - -import asyncio -import sys - -import anyio -import anyio.abc -import grpc -import grpc.aio - - -def main() -> None: - """Exit unsuccessfully when a native completion targets an earlier, closed loop.""" - channels: list[grpc.aio.Channel] = [] - errors: list[dict[str, object]] = [] - - async def run() -> None: - asyncio.get_running_loop().set_exception_handler(lambda loop, context: errors.append(context)) - channel = grpc.aio.insecure_channel("127.0.0.1:1") - channels.append(channel) - - async def watch(*, task_status: anyio.abc.TaskStatus[None] = anyio.TASK_STATUS_IGNORED) -> None: - task_status.started() - await channel.wait_for_state_change(channel.get_state()) - - async with anyio.create_task_group() as tg: - await tg.start(watch) - tg.cancel_scope.cancel() - await channel.close() - - for _ in range(10): - anyio.run(run) - assert not errors, (grpc.__version__, sys.version, errors) - - -if __name__ == "__main__": - main() diff --git a/examples/transports/tests/__init__.py b/examples/transports/tests/__init__.py deleted file mode 100644 index a9a2c5b3bb..0000000000 --- a/examples/transports/tests/__init__.py +++ /dev/null @@ -1 +0,0 @@ -__all__ = [] diff --git a/examples/transports/tests/cassettes/test_grpc/test_native_error_keeps_code_message_and_data.yaml b/examples/transports/tests/cassettes/test_grpc/test_native_error_keeps_code_message_and_data.yaml deleted file mode 100644 index 11fd028a43..0000000000 --- a/examples/transports/tests/cassettes/test_grpc/test_native_error_keeps_code_message_and_data.yaml +++ /dev/null @@ -1,19 +0,0 @@ -version: 1 -interactions: [] -grpc_interactions: - - request: - method: /mcp.transport.example.MCP/Call - metadata: {} - body: - type: binary - content: >- - 0a0e6578616d706c652f72656675736512b8017b225f6d657461223a7b22696f2e6d6f64656c636f6e7465787470726f746f636f6c2f70726f746f636f6c56657273696f6e223a22323032362d30372d3238222c22696f2e6d6f64656c636f6e7465787470726f746f636f6c2f636c69656e74496e666f223a7b226e616d65223a226d6370222c2276657273696f6e223a22302e312e30227d2c22696f2e6d6f64656c636f6e7465787470726f746f636f6c2f636c69656e744361706162696c6974696573223a7b7d7d7d1a0130 - response: - status_code: 0 - status_message: OK - metadata: {} - body: - type: binary - content: >- - 0000007912777b22636f6465223a2d313039393531313632373737362c226d657373616765223a226170706c69636174696f6e207265667573616c222c2264617461223a7b2276656e646f722f726561736f6e223a226361706163697479222c226c61726765223a393232333337323033363835343737353830397d7d - recorded_at: 2026-09-16T16:13:04.953959+00:00 diff --git a/examples/transports/tests/cassettes/test_grpc/test_native_payload_keeps_large_integers_and_extension_fields.yaml b/examples/transports/tests/cassettes/test_grpc/test_native_payload_keeps_large_integers_and_extension_fields.yaml deleted file mode 100644 index ef9d397814..0000000000 --- a/examples/transports/tests/cassettes/test_grpc/test_native_payload_keeps_large_integers_and_extension_fields.yaml +++ /dev/null @@ -1,19 +0,0 @@ -version: 1 -interactions: [] -grpc_interactions: - - request: - method: /mcp.transport.example.MCP/Call - metadata: {} - body: - type: binary - content: >- - 0a0c6578616d706c652f6563686f128a027b2276616c7565223a7b226c61726765223a393232333337323033363835343737353830392c2276656e646f722f6669656c64223a5b6e756c6c2c7b226c6162656c223a226361665c7530306539227d5d7d2c225f6d657461223a7b22696f2e6d6f64656c636f6e7465787470726f746f636f6c2f70726f746f636f6c56657273696f6e223a22323032362d30372d3238222c22696f2e6d6f64656c636f6e7465787470726f746f636f6c2f636c69656e74496e666f223a7b226e616d65223a226d6370222c2276657273696f6e223a22302e312e30227d2c22696f2e6d6f64656c636f6e7465787470726f746f636f6c2f636c69656e744361706162696c6974696573223a7b7d7d7d1a0130 - response: - status_code: 0 - status_message: OK - metadata: {} - body: - type: binary - content: >- - 000000bc0ab9017b2276616c7565223a7b226c61726765223a393232333337323033363835343737353830392c2276656e646f722f6669656c64223a5b6e756c6c2c7b226c6162656c223a226361665c7530306539227d5d7d2c22726573756c7454797065223a22636f6d706c657465222c225f6d657461223a7b22696f2e6d6f64656c636f6e7465787470726f746f636f6c2f736572766572496e666f223a7b226e616d65223a226e6174697665222c2276657273696f6e223a22227d7d7d - recorded_at: 2026-09-16T16:13:04.939254+00:00 diff --git a/examples/transports/tests/cassettes/test_grpc/test_native_progress_reaches_the_client_before_the_result.yaml b/examples/transports/tests/cassettes/test_grpc/test_native_progress_reaches_the_client_before_the_result.yaml deleted file mode 100644 index 2b57775b97..0000000000 --- a/examples/transports/tests/cassettes/test_grpc/test_native_progress_reaches_the_client_before_the_result.yaml +++ /dev/null @@ -1,19 +0,0 @@ -version: 1 -interactions: [] -grpc_interactions: - - request: - method: /mcp.transport.example.MCP/Call - metadata: {} - body: - type: binary - content: >- - 0a0a746f6f6c732f63616c6c12ee017b226e616d65223a226563686f222c22617267756d656e7473223a7b2276616c7565223a226e61746976652070726f6772657373227d2c225f6d657461223a7b22696f2e6d6f64656c636f6e7465787470726f746f636f6c2f70726f746f636f6c56657273696f6e223a22323032362d30372d3238222c22696f2e6d6f64656c636f6e7465787470726f746f636f6c2f636c69656e74496e666f223a7b226e616d65223a226d6370222c2276657273696f6e223a22302e312e30227d2c22696f2e6d6f64656c636f6e7465787470726f746f636f6c2f636c69656e744361706162696c6974696573223a7b7d7d7d1a01302001 - response: - status_code: 0 - status_message: OK - metadata: {} - body: - type: binary - content: >- - 0000005a1a580a166e6f74696669636174696f6e732f70726f6772657373123e7b2270726f6772657373546f6b656e223a302c2270726f6772657373223a312c22746f74616c223a322c226d657373616765223a2268616c66776179227d000000e90ae6017b22636f6e74656e74223a5b7b2274657874223a226e61746976652070726f6772657373222c2274797065223a2274657874227d5d2c2269734572726f72223a66616c73652c22726573756c7454797065223a22636f6d706c657465222c2273747275637475726564436f6e74656e74223a7b22726573756c74223a226e61746976652070726f6772657373227d2c225f6d657461223a7b22696f2e6d6f64656c636f6e7465787470726f746f636f6c2f736572766572496e666f223a7b226e616d65223a226e61746976652d70726f6772657373222c2276657273696f6e223a22227d7d7d - recorded_at: 2026-09-16T16:13:04.948671+00:00 diff --git a/examples/transports/tests/conftest.py b/examples/transports/tests/conftest.py deleted file mode 100644 index c4f226d6db..0000000000 --- a/examples/transports/tests/conftest.py +++ /dev/null @@ -1,14 +0,0 @@ -from collections.abc import AsyncIterator - -import pytest - - -@pytest.fixture(scope="session") -def anyio_backend() -> str: - return "asyncio" - - -@pytest.fixture(scope="session", autouse=True) -async def grpc_event_loop(anyio_backend: str) -> AsyncIterator[None]: - """Keep gRPC's process-wide completion queue on one loop, including late connectivity callbacks.""" - yield diff --git a/examples/transports/tests/test_amqp.py b/examples/transports/tests/test_amqp.py deleted file mode 100644 index 11b0a346e5..0000000000 --- a/examples/transports/tests/test_amqp.py +++ /dev/null @@ -1,30 +0,0 @@ -import pytest -from aio_pika import Channel, Connection -from yarl import URL - -from mcp_transport_examples.amqp import amqp_transport - - -@pytest.mark.anyio -@pytest.mark.parametrize( - ("incoming", "outgoing", "expiry", "size", "confirms"), - [ - ("", "responses", 60, 4096, True), - ("requests", "", 60, 4096, True), - ("same", "same", 60, 4096, True), - ("requests", "responses", 0, 4096, True), - ("requests", "responses", 60, 0, True), - ("requests", "responses", 60, 4096, False), - ], -) -async def test_invalid_configuration_fails_without_connecting( - incoming: str, outgoing: str, expiry: int, size: int, confirms: bool -) -> None: - """Adapter-defined constraints reject unsafe queue routing and limits before touching a broker.""" - connection = Connection(URL("amqp://unused.invalid")) - channel = Channel(connection, publisher_confirms=confirms) - with pytest.raises(ValueError): - async with amqp_transport( - channel, incoming_queue=incoming, outgoing_queue=outgoing, expiry=expiry, max_message_size=size - ): - raise NotImplementedError diff --git a/examples/transports/tests/test_grpc.py b/examples/transports/tests/test_grpc.py deleted file mode 100644 index 972f01b22c..0000000000 --- a/examples/transports/tests/test_grpc.py +++ /dev/null @@ -1,185 +0,0 @@ -import json -from collections.abc import AsyncIterator, Callable -from contextlib import AsyncExitStack, asynccontextmanager -from typing import Any - -import anyio -import grpc.aio -import pytest -from cassetter import Cassette, Cassetter -from mcp import Client, MCPError -from mcp.server import Server, ServerRequestContext -from mcp.server.mcpserver import Context, MCPServer -from mcp.types import ( - CONNECTION_CLOSED, - CallToolRequest, - CallToolRequestParams, - CallToolResult, - Request, - RequestParams, - Result, -) - -from mcp_transport_examples.grpc import grpc_client, grpc_server -from mcp_transport_examples.rpc_pb2 import CallEvent, CallRequest - - -@pytest.fixture(scope="module") -def vcr_config() -> Cassetter: - return Cassetter(intercept=["grpc"]) - - -@asynccontextmanager -async def connected( - server: Server[Any] | MCPServer[Any], cassette: Cassette, monkeypatch: pytest.MonkeyPatch -) -> AsyncIterator[Client]: - async with AsyncExitStack() as stack: - listener = grpc.aio.server() - port = listener.add_insecure_port("127.0.0.1:0") - stack.push_async_callback(listener.stop, 0) - runtime = await stack.enter_async_context(server.serve()) - await runtime.connect(grpc_server(listener)) - await listener.start() - channel = await stack.enter_async_context(grpc.aio.insecure_channel(f"127.0.0.1:{port}")) - requests: list[CallRequest] = [] - unary_stream = channel.unary_stream - - def capture( - method: str, - request_serializer: Callable[[CallRequest], bytes] | None = None, - response_deserializer: Callable[[bytes], CallEvent] | None = None, - ) -> grpc.aio.UnaryStreamMultiCallable[CallRequest, CallEvent]: - assert request_serializer is not None - - def serialize(request: CallRequest) -> bytes: - payload = request_serializer(request) - requests.append(CallRequest.FromString(payload)) - return payload - - return unary_stream(method, request_serializer=serialize, response_deserializer=response_deserializer) - - monkeypatch.setattr(channel, "unary_stream", capture) - client = await stack.enter_async_context(Client(grpc_client(channel), mode="2026-07-28")) - yield client - assert requests - assert len(cassette.grpc_interactions) == 1 - payload = cassette.grpc_interactions[0].request.body.content - assert isinstance(payload, bytes) - recorded = CallRequest.FromString(payload) - for request in requests: - # cassetter currently matches gRPC methods, not request bodies. - assert request.method == recorded.method - assert json.loads(request.params_json) == json.loads(recorded.params_json) - assert request.request_id_json == recorded.request_id_json - assert request.report_progress == recorded.report_progress - - -@pytest.mark.anyio -@pytest.mark.vcr -async def test_native_payload_keeps_large_integers_and_extension_fields( - cassette: Cassette, monkeypatch: pytest.MonkeyPatch -) -> None: - """A recorded real RPC preserves arbitrary MCP payload fields without protobuf Struct's float conversion.""" - - class EchoParams(RequestParams): - value: dict[str, Any] - - class EchoResult(Result): - value: dict[str, Any] - - async def echo(ctx: ServerRequestContext, params: EchoParams) -> EchoResult: - assert ctx.method == "example/echo" - return EchoResult(value=params.value) - - server = Server("native") - server.add_request_handler("example/echo", EchoParams, echo) - payload = {"large": 2**63 + 1, "vendor/field": [None, {"label": "café"}]} - with anyio.fail_after(5): - async with connected(server, cassette, monkeypatch) as client: - result = await client.session.send_request( - Request(method="example/echo", params=EchoParams(value=payload)), EchoResult - ) - assert result.value == payload - async with Client(server, mode="2026-07-28") as local: - expected = await local.session.send_request( - Request(method="example/echo", params=EchoParams(value=payload)), EchoResult - ) - assert result == expected - - -@pytest.mark.anyio -@pytest.mark.vcr -async def test_native_progress_reaches_the_client_before_the_result( - cassette: Cassette, monkeypatch: pytest.MonkeyPatch -) -> None: - """A recorded response stream routes progress through the SDK callback, isolated from tools/list schema fetching.""" - server = MCPServer("native-progress") - - @server.tool() - async def echo(value: str, ctx: Context) -> str: - await ctx.report_progress(1, 2, "halfway") - return value - - updates: list[tuple[float, float | None, str | None]] = [] - - async def progress(progress: float, total: float | None, message: str | None) -> None: - updates.append((progress, total, message)) - - value = "native progress" - with anyio.fail_after(5): - async with connected(server, cassette, monkeypatch) as client: - result = await client.session.send_request( - CallToolRequest(params=CallToolRequestParams(name="echo", arguments={"value": value})), - CallToolResult, - progress_callback=progress, - ) - assert result.structured_content == {"result": value} - wire_updates = updates.copy() - updates.clear() - async with Client(server, mode="2026-07-28") as local: - expected = await local.session.send_request( - CallToolRequest(params=CallToolRequestParams(name="echo", arguments={"value": value})), - CallToolResult, - progress_callback=progress, - ) - assert result == expected - assert updates == wire_updates == [(1, 2, "halfway")] - - -@pytest.mark.anyio -async def test_request_immediately_after_channel_close_reports_mcp_connection_closed() -> None: - """An idle borrowed channel closes without a network request or a scheduling opportunity for its watcher.""" - with anyio.fail_after(5): - async with grpc.aio.insecure_channel("unused.invalid:50051") as channel: - async with Client(grpc_client(channel), mode="2026-07-28") as client: - await channel.close() - with pytest.raises(MCPError) as exc: - await client.list_tools() - assert exc.value.code == CONNECTION_CLOSED - - -@pytest.mark.anyio -@pytest.mark.vcr -async def test_native_error_keeps_code_message_and_data(cassette: Cassette, monkeypatch: pytest.MonkeyPatch) -> None: - """Native application errors retain all MCP fields, including codes outside signed int32.""" - code = -(2**40) - message = "application refusal" - data = {"vendor/reason": "capacity", "large": 2**63 + 1} - - async def refuse(ctx: ServerRequestContext, params: RequestParams) -> Result: - assert ctx.method == "example/refuse" - raise MCPError(code=code, message=message, data=data) - - server = Server("native-errors") - server.add_request_handler("example/refuse", RequestParams, refuse) - with anyio.fail_after(5): - async with connected(server, cassette, monkeypatch) as client: - with pytest.raises(MCPError) as exc: - await client.session.send_request(Request(method="example/refuse", params=RequestParams()), Result) - assert exc.value.code == code - assert exc.value.message == message - assert exc.value.data == data - async with Client(server, mode="2026-07-28") as local: - with pytest.raises(MCPError) as expected: - await local.session.send_request(Request(method="example/refuse", params=RequestParams()), Result) - assert exc.value.error == expected.value.error diff --git a/examples/transports/tests/test_grpc_cancel_signal.py b/examples/transports/tests/test_grpc_cancel_signal.py deleted file mode 100644 index 2bc9e9e6e9..0000000000 --- a/examples/transports/tests/test_grpc_cancel_signal.py +++ /dev/null @@ -1,72 +0,0 @@ -"""Peer cancellation must be visible before the handler's cleanup runs.""" - -from collections.abc import Mapping -from typing import Any - -import anyio -import anyio.abc -import grpc.aio -import pytest -from mcp import Client -from mcp.shared.dispatcher import DispatchContext -from mcp.shared.transport import TransportContext -from mcp.types import Request, RequestParams, Result - -from mcp_transport_examples.grpc import grpc_client -from mcp_transport_examples.grpc_server import GRPCServerDispatcher - - -async def verify() -> None: - entered = anyio.Event() - done = anyio.Event() - observed: list[bool] = [] - - async def handle( - ctx: DispatchContext[TransportContext], method: str, params: Mapping[str, Any] | None - ) -> dict[str, Any]: - assert method == "example/wait" - entered.set() - try: - await anyio.sleep_forever() - finally: - observed.append(ctx.cancel_requested.is_set()) - done.set() - raise NotImplementedError - - async def notify(ctx: DispatchContext[TransportContext], method: str, params: Mapping[str, Any] | None) -> None: - raise NotImplementedError - - listener = grpc.aio.server() - port = listener.add_insecure_port("127.0.0.1:0") - dispatcher = GRPCServerDispatcher(listener) - try: - async with anyio.create_task_group() as tg: - await tg.start(dispatcher.run, handle, notify) - await listener.start() - async with grpc.aio.insecure_channel(f"127.0.0.1:{port}") as channel: - async with Client(grpc_client(channel), mode="2026-07-28") as client: - - async def call(*, task_status: anyio.abc.TaskStatus[anyio.CancelScope]) -> None: - with anyio.CancelScope() as scope: - task_status.started(scope) - await client.session.send_request( - Request(method="example/wait", params=RequestParams()), Result - ) - - async with anyio.create_task_group() as calls: - scope = await calls.start(call) - await entered.wait() - scope.cancel() - await done.wait() - assert observed == [True] - tg.cancel_scope.cancel() - finally: - with anyio.move_on_after(5, shield=True): - await listener.stop(0) - - -@pytest.mark.anyio -async def test_peer_cancellation_is_signalled_before_handler_cleanup() -> None: - """Read cancel_requested during the live handler's cleanup, not after RPC completion.""" - with anyio.fail_after(5): - await verify() diff --git a/examples/transports/tests/test_grpc_client.py b/examples/transports/tests/test_grpc_client.py deleted file mode 100644 index 2148658be9..0000000000 --- a/examples/transports/tests/test_grpc_client.py +++ /dev/null @@ -1,106 +0,0 @@ -from contextlib import AsyncExitStack -from typing import Any - -import anyio -import grpc.aio -import pytest -from mcp.client import ClientSession -from mcp.server.mcpserver import Context, MCPServer -from mcp.shared.exceptions import MCPError, NoBackChannelError -from mcp.types import CLIENT_CAPABILITIES_META_KEY, CONNECTION_CLOSED, PROTOCOL_VERSION_META_KEY, CallToolRequestParams - -from mcp_transport_examples.grpc import grpc_client, grpc_server - - -@pytest.mark.anyio -@pytest.mark.parametrize("closed_channel", [False, True], ids=["session-exit", "closed-channel-startup"]) -async def test_unstarted_and_closed_dispatchers_never_issue_an_rpc(closed_channel: bool) -> None: - """Public dispatcher guards reject requests before startup and drop notifications after closure without dialing.""" - with anyio.fail_after(5): - async with ( - grpc.aio.insecure_channel("unused.invalid:50051") as channel, - grpc_client(channel).connection as dispatcher, - ): - with pytest.raises(RuntimeError): - await dispatcher.send_raw_request("example/test", None) - with pytest.raises(NoBackChannelError): - await dispatcher.notify("example/event", None) - if closed_channel: - await channel.close() - async with ClientSession(dispatcher=dispatcher): - pass - await dispatcher.notify("example/event", None) - with pytest.raises(MCPError) as exc: - await dispatcher.send_raw_request("example/test", None) - assert exc.value.code == CONNECTION_CLOSED - - -@pytest.mark.anyio -@pytest.mark.parametrize("value", ["a" * (4 * 1024 * 1024), float("nan")], ids=["oversized", "nonfinite"]) -async def test_invalid_outgoing_payload_fails_before_dialing(value: str | float) -> None: - """The raw dispatcher rejects invalid JSON; the typed client normalizes nonfinite values before this boundary.""" - with anyio.fail_after(5): - async with ( - grpc.aio.insecure_channel("unused.invalid:50051") as channel, - grpc_client(channel).connection as dispatcher, - ClientSession(dispatcher=dispatcher), - ): - with pytest.raises(ValueError): - await dispatcher.send_raw_request("example/test", {"value": value}) - assert channel.get_state() == grpc.ChannelConnectivity.IDLE - - -@pytest.mark.anyio -async def test_request_ids_preserve_spelling_and_reject_in_flight_collisions() -> None: - """Exercise the public dispatcher option that the high-level client normally supplies for subscriptions.""" - entered = anyio.Event() - release = anyio.Event() - server = MCPServer("request IDs") - - @server.tool() - async def identify(hold: bool, ctx: Context) -> str | int | None: - if hold: - entered.set() - await release.wait() - return ctx.request_context.request_id - - waiting = CallToolRequestParams( - name="identify", - arguments={"hold": True}, - _meta={PROTOCOL_VERSION_META_KEY: "2026-07-28", CLIENT_CAPABILITIES_META_KEY: {}}, - ).model_dump(by_alias=True, exclude_none=True) - immediate = CallToolRequestParams( - name="identify", - arguments={"hold": False}, - _meta={PROTOCOL_VERSION_META_KEY: "2026-07-28", CLIENT_CAPABILITIES_META_KEY: {}}, - ).model_dump(by_alias=True, exclude_none=True) - results: list[dict[str, Any]] = [] - - with anyio.fail_after(5): - async with AsyncExitStack() as stack: - listener = grpc.aio.server() - port = listener.add_insecure_port("127.0.0.1:0") - stack.push_async_callback(listener.stop, 0) - runtime = await stack.enter_async_context(server.serve()) - await runtime.connect(grpc_server(listener)) - await listener.start() - channel = await stack.enter_async_context(grpc.aio.insecure_channel(f"127.0.0.1:{port}")) - dispatcher = await stack.enter_async_context(grpc_client(channel).connection) - await stack.enter_async_context(ClientSession(dispatcher=dispatcher)) - - async def first() -> None: - results.append(await dispatcher.send_raw_request("tools/call", waiting, {"request_id": 0})) - - async with anyio.create_task_group() as tg: - tg.start_soon(first) - try: - await entered.wait() - with pytest.raises(ValueError): - await dispatcher.send_raw_request("tools/call", immediate, {"request_id": "0"}) - minted = await dispatcher.send_raw_request("tools/call", immediate) - assert minted["structuredContent"] == {"result": 1} - finally: - release.set() - assert results[0]["structuredContent"] == {"result": 0} - reused = await dispatcher.send_raw_request("tools/call", immediate, {"request_id": "0"}) - assert reused["structuredContent"] == {"result": "0"} diff --git a/examples/transports/tests/test_grpc_client_shutdown.py b/examples/transports/tests/test_grpc_client_shutdown.py deleted file mode 100644 index bf941c0540..0000000000 --- a/examples/transports/tests/test_grpc_client_shutdown.py +++ /dev/null @@ -1,94 +0,0 @@ -"""Client shutdown must interrupt callbacks as well as gRPC socket reads.""" - -import anyio -import anyio.abc -import grpc.aio -import pytest -from mcp import Client, MCPError -from mcp.server.mcpserver import Context, MCPServer -from mcp.types import CONNECTION_CLOSED - -from mcp_transport_examples.grpc import grpc_client, grpc_server - - -async def verify(*, shield_cleanup: bool = False) -> None: - cleanup_started = anyio.Event() - release_cleanup = anyio.Event() - callback_entered = anyio.Event() - callback_cancelled = anyio.Event() - close_client = anyio.Event() - client_closed = anyio.Event() - call_finished = anyio.Event() - server_cancelled = anyio.Event() - server = MCPServer("client shutdown") - - @server.tool() - async def wait(ctx: Context) -> str: - try: - await ctx.report_progress(1, 2) - await anyio.sleep_forever() - finally: - server_cancelled.set() - raise NotImplementedError - - async def progress(progress: float, total: float | None, message: str | None) -> None: - callback_entered.set() - try: - await anyio.sleep_forever() - finally: - if shield_cleanup: - with anyio.CancelScope(shield=True): - cleanup_started.set() - await release_cleanup.wait() - callback_cancelled.set() - - listener = grpc.aio.server() - port = listener.add_insecure_port("127.0.0.1:0") - try: - async with server.serve() as runtime: - await runtime.connect(grpc_server(listener)) - await listener.start() - async with grpc.aio.insecure_channel(f"127.0.0.1:{port}") as channel: - - async def own_client(*, task_status: anyio.abc.TaskStatus[Client]) -> None: - async with Client(grpc_client(channel), mode="2026-07-28") as client: - task_status.started(client) - await close_client.wait() - client_closed.set() - - async def call(client: Client) -> None: - with pytest.raises(MCPError) as exc: - await client.call_tool("wait", progress_callback=progress) - assert exc.value.code == CONNECTION_CLOSED - call_finished.set() - - async with anyio.create_task_group() as tg: - client = await tg.start(own_client) - tg.start_soon(call, client) - try: - await callback_entered.wait() - close_client.set() - if shield_cleanup: - await cleanup_started.wait() - # Shutdown must wait for this callback, not abandon it after five seconds. - with anyio.move_on_after(5.1) as window: - await client_closed.wait() - assert window.cancelled_caught - finally: - release_cleanup.set() - await client_closed.wait() - await call_finished.wait() - await server_cancelled.wait() - assert callback_cancelled.is_set() - finally: - with anyio.move_on_after(5, shield=True): - await listener.stop(0) - - -@pytest.mark.anyio -@pytest.mark.parametrize("shield_cleanup", [False, True]) -async def test_client_shutdown_joins_blocked_callbacks(shield_cleanup: bool) -> None: - """The client must wait for callback cleanup before relinquishing its session resources.""" - # The shielded case deliberately exceeds the former five-second join deadline. - with anyio.fail_after(10): - await verify(shield_cleanup=shield_cleanup) diff --git a/examples/transports/tests/test_grpc_context.py b/examples/transports/tests/test_grpc_context.py deleted file mode 100644 index 9fba9c4dc4..0000000000 --- a/examples/transports/tests/test_grpc_context.py +++ /dev/null @@ -1,51 +0,0 @@ -from collections.abc import Mapping -from typing import Any - -import anyio -import grpc.aio -import pytest -from mcp.shared.exceptions import NoBackChannelError - -from mcp_transport_examples.grpc import grpc_server -from mcp_transport_examples.grpc_context import GRPCContext, GRPCDispatchContext - - -@pytest.mark.anyio -async def test_modern_binding_refuses_server_requests_and_unscoped_notifications() -> None: - """The public dispatcher and context refuse channels that the native modern binding does not provide.""" - listener = grpc.aio.server() - - async def notify(method: str, params: Mapping[str, Any] | None) -> None: - raise NotImplementedError - - context = GRPCDispatchContext(GRPCContext(kind="grpc", can_send_request=False, peer="test"), 1, notify) - with anyio.fail_after(5): - with pytest.raises(NoBackChannelError): - await context.send_raw_request("example/request", None) - async with grpc_server(listener).connection as dispatcher: - with pytest.raises(NoBackChannelError): - await dispatcher.send_raw_request("example/request", None) - with pytest.raises(NoBackChannelError): - await dispatcher.notify("example/event", None) - assert not context.can_send_request - - -@pytest.mark.anyio -@pytest.mark.parametrize("report_progress", [False, True]) -async def test_context_progress_is_opt_in_and_omits_absent_fields(report_progress: bool) -> None: - """Progress without an opt-in is a no-op; supplied values are forwarded without inventing total or message.""" - notifications: list[tuple[str, Mapping[str, Any] | None]] = [] - - async def notify(method: str, params: Mapping[str, Any] | None) -> None: - notifications.append((method, params)) - - context = GRPCDispatchContext( - GRPCContext(kind="grpc", can_send_request=False, peer="test"), - "request", - notify, - report_progress=report_progress, - ) - await context.progress(1) - assert notifications == ( - [("notifications/progress", {"progressToken": "request", "progress": 1})] if report_progress else [] - ) diff --git a/examples/transports/tests/test_grpc_lifecycle.py b/examples/transports/tests/test_grpc_lifecycle.py deleted file mode 100644 index e9193a2fd6..0000000000 --- a/examples/transports/tests/test_grpc_lifecycle.py +++ /dev/null @@ -1,95 +0,0 @@ -"""Live cancellation and shutdown checks that require the current gRPC server to execute.""" - -from collections.abc import AsyncIterator -from contextlib import asynccontextmanager - -import anyio -import anyio.abc -import grpc.aio -import pytest -from mcp import Client, MCPError -from mcp.server.mcpserver import MCPServer -from mcp.types import CONNECTION_CLOSED, REQUEST_TIMEOUT - -from mcp_transport_examples.grpc import grpc_client, grpc_server - - -async def verify(cause: str) -> None: - entered = anyio.Event() - cancelled = anyio.Event() - stop = anyio.Event() - stopped = anyio.Event() - finished = anyio.Event() - errors: list[int] = [] - - @asynccontextmanager - async def lifespan(server: MCPServer[None]) -> AsyncIterator[None]: - try: - yield None - finally: - assert cancelled.is_set() - - server = MCPServer("gRPC cancellation", lifespan=lifespan) - - @server.tool() - async def wait() -> str: - entered.set() - try: - await anyio.sleep_forever() - finally: - cancelled.set() - raise NotImplementedError - - listener = grpc.aio.server() - port = listener.add_insecure_port("127.0.0.1:0") - - async def run_server(*, task_status: anyio.abc.TaskStatus[None]) -> None: - try: - async with server.serve() as runtime: - await runtime.connect(grpc_server(listener)) - await listener.start() - task_status.started() - await stop.wait() - finally: - with anyio.move_on_after(5, shield=True): - await listener.stop(0) - stopped.set() - - async with anyio.create_task_group() as tg: - await tg.start(run_server) - async with grpc.aio.insecure_channel(f"127.0.0.1:{port}") as channel: - async with Client(grpc_client(channel), mode="2026-07-28") as client: - - async def call(*, task_status: anyio.abc.TaskStatus[anyio.CancelScope]) -> None: - with anyio.CancelScope() as scope: - task_status.started(scope) - try: - # A real deadline is the behavior under test, not a synchronization delay. - await client.call_tool("wait", read_timeout_seconds=0.5 if cause == "timeout" else None) - except MCPError as exc: - errors.append(exc.code) - finished.set() - - async with anyio.create_task_group() as calls: - scope = await calls.start(call) - await entered.wait() - if cause == "caller": - scope.cancel() - elif cause == "runtime": - stop.set() - elif cause == "channel": - await channel.close() - await finished.wait() - await cancelled.wait() - expected = [] if cause == "caller" else [REQUEST_TIMEOUT if cause == "timeout" else CONNECTION_CLOSED] - assert errors == expected - stop.set() - await stopped.wait() - - -@pytest.mark.anyio -@pytest.mark.parametrize("cause", ["caller", "timeout", "runtime", "channel"]) -async def test_native_cancellation_finishes_the_request_and_handler(cause: str) -> None: - """Exercise this process's gRPC server, not a recorded response or an external service.""" - with anyio.fail_after(5): - await verify(cause) diff --git a/examples/transports/tests/test_grpc_response.py b/examples/transports/tests/test_grpc_response.py deleted file mode 100644 index cd063631ec..0000000000 --- a/examples/transports/tests/test_grpc_response.py +++ /dev/null @@ -1,84 +0,0 @@ -from collections.abc import AsyncIterator - -import anyio -import grpc -import grpc.aio -import pytest -from mcp import Client, MCPError -from mcp.types import CONNECTION_CLOSED, Request, RequestParams, Result - -from mcp_transport_examples.grpc import grpc_client -from mcp_transport_examples.rpc_pb2 import CallEvent, CallRequest, Notification - - -@pytest.mark.anyio -@pytest.mark.parametrize( - "events", - [ - [], - [CallEvent()], - [CallEvent(result_json=b"{}"), CallEvent(result_json=b"{}")], - [CallEvent(result_json=b"[]")], - [CallEvent(result_json=b'{"value": Infinity}')], - [CallEvent(notification=Notification(method="example/event", params_json=b"[]"))], - [ - CallEvent( - notification=Notification( - method="notifications/progress", params_json=b'{"progressToken":0,"progress":1}' - ) - ) - ], - [CallEvent(result_json=b'{"padding":"' + b"a" * (4 * 1024 * 1024) + b'"}')], - ], - ids=[ - "missing", - "empty-event", - "duplicate", - "array-result", - "nonfinite", - "bad-notification", - "notification-only", - "oversized", - ], -) -async def test_invalid_response_frames_fail_the_mcp_request(events: list[CallEvent]) -> None: - """A typed SDK server cannot produce these invalid frames, so a local gRPC peer sends them explicitly.""" - - async def reply( - request: CallRequest, context: grpc.aio.ServicerContext[CallRequest, CallEvent] - ) -> AsyncIterator[CallEvent]: - assert request.method == "example/response" - for event in events: - yield event - - listener = grpc.aio.server() - listener.add_generic_rpc_handlers( - [ - grpc.method_handlers_generic_handler( - "mcp.transport.example.MCP", - { - "Call": grpc.unary_stream_rpc_method_handler( - reply, - request_deserializer=CallRequest.FromString, - response_serializer=CallEvent.SerializeToString, - ) - }, - ) - ] - ) - port = listener.add_insecure_port("127.0.0.1:0") - with anyio.fail_after(5): - try: - await listener.start() - async with grpc.aio.insecure_channel( - f"127.0.0.1:{port}", options=[("grpc.max_receive_message_length", 8 * 1024 * 1024)] - ) as channel: - async with Client(grpc_client(channel), mode="2026-07-28") as client: - with pytest.raises(MCPError) as exc: - await client.session.send_request( - Request(method="example/response", params=RequestParams()), Result - ) - assert exc.value.code == CONNECTION_CLOSED - assert isinstance(exc.value.__cause__, ValueError) - finally: - await listener.stop(0) diff --git a/examples/transports/tests/test_grpc_server.py b/examples/transports/tests/test_grpc_server.py deleted file mode 100644 index 4ae600297b..0000000000 --- a/examples/transports/tests/test_grpc_server.py +++ /dev/null @@ -1,220 +0,0 @@ -import json -from collections.abc import AsyncIterator -from contextlib import AsyncExitStack, asynccontextmanager -from typing import Any - -import anyio -import grpc -import grpc.aio -import pytest -from mcp import Client, MCPError -from mcp.client.subscriptions import ToolsListChanged -from mcp.server import Server, ServerRequestContext -from mcp.server.mcpserver import Context, MCPServer -from mcp.types import CONNECTION_CLOSED, INVALID_PARAMS, Request, RequestParams, Result -from pydantic import BaseModel - -from mcp_transport_examples.grpc import grpc_client, grpc_server -from mcp_transport_examples.rpc_pb2 import CallEvent, CallRequest - - -@asynccontextmanager -async def serving(server: Server[Any] | MCPServer[Any], *, max_requests: int = 64) -> AsyncIterator[str]: - async with AsyncExitStack() as stack: - listener = grpc.aio.server(options=[("grpc.max_receive_message_length", 8 * 1024 * 1024)]) - port = listener.add_insecure_port("127.0.0.1:0") - stack.push_async_callback(listener.stop, 0) - runtime = await stack.enter_async_context(server.serve()) - await runtime.connect(grpc_server(listener, max_requests=max_requests)) - await listener.start() - yield f"127.0.0.1:{port}" - - -@pytest.mark.anyio -@pytest.mark.parametrize( - ("params", "request_id"), - [ - (b"[]", b"0"), - (b'{"number": NaN}', b"0"), - (b"\xff", b"0"), - (b"{}", b"true"), - (b"{}", b"null"), - (b'{"padding":"' + b"a" * (4 * 1024 * 1024) + b'"}', b"0"), - ], - ids=["array", "nonfinite", "invalid-utf8", "boolean-id", "null-id", "oversized"], -) -async def test_invalid_binding_payload_is_rejected_before_mcp_dispatch(params: bytes, request_id: bytes) -> None: - """The typed MCP client cannot emit malformed protobuf-binding input, so send it over a real raw gRPC call.""" - with anyio.fail_after(5): - async with serving(Server("validation")) as target, grpc.aio.insecure_channel(target) as channel: - call = channel.unary_stream( - "/mcp.transport.example.MCP/Call", - request_serializer=CallRequest.SerializeToString, - response_deserializer=CallEvent.FromString, - ) - with pytest.raises(grpc.aio.AioRpcError) as exc: - async for _ in call( - CallRequest(method="example/invalid", params_json=params, request_id_json=request_id) - ): - raise NotImplementedError - assert exc.value.code() == grpc.StatusCode.INVALID_ARGUMENT - - -@pytest.mark.anyio -async def test_native_capacity_rejects_excess_work_and_recovers() -> None: - """A saturated binding refuses another request without preventing the admitted one from completing.""" - entered = anyio.Event() - release = anyio.Event() - server = MCPServer("capacity") - - @server.tool() - async def hold() -> str: - entered.set() - await release.wait() - return "released" - - with anyio.fail_after(5): - async with serving(server, max_requests=1) as target, grpc.aio.insecure_channel(target) as channel: - async with Client(grpc_client(channel), mode="2026-07-28") as client: - - async def first() -> None: - result = await client.call_tool("hold") - assert result.structured_content == {"result": "released"} - - async with anyio.create_task_group() as tg: - tg.start_soon(first) - try: - await entered.wait() - with pytest.raises(MCPError) as exc: - await client.list_tools() - assert exc.value.code == CONNECTION_CLOSED - cause = exc.value.__cause__ - assert isinstance(cause, grpc.aio.AioRpcError) - assert cause.code() == grpc.StatusCode.RESOURCE_EXHAUSTED - finally: - release.set() - tools = await client.list_tools() - assert [tool.name for tool in tools.tools] == ["hold"] - - -@pytest.mark.anyio -async def test_closed_runtime_refuses_calls_on_a_borrowed_listener() -> None: - """Runtime shutdown closes the MCP binding while leaving the caller's gRPC listener under its ownership.""" - listener = grpc.aio.server() - port = listener.add_insecure_port("127.0.0.1:0") - with anyio.fail_after(5): - try: - async with Server("closed runtime").serve() as runtime: - await runtime.connect(grpc_server(listener)) - await listener.start() - async with grpc.aio.insecure_channel(f"127.0.0.1:{port}") as channel: - async with Client(grpc_client(channel), mode="2026-07-28") as client: - with pytest.raises(MCPError) as exc: - await client.list_tools() - assert exc.value.code == CONNECTION_CLOSED - cause = exc.value.__cause__ - assert isinstance(cause, grpc.aio.AioRpcError) - assert cause.code() == grpc.StatusCode.UNAVAILABLE - finally: - await listener.stop(0) - - -@pytest.mark.anyio -@pytest.mark.parametrize("failure", ["mcp", "validation", "self-cancel"]) -async def test_handler_failures_settle_the_native_call(failure: str) -> None: - """Run the current server's failure paths; a replayed response would not exercise handler lifetime or conversion.""" - - class IntegerValue(BaseModel): - value: int - - async def handler(ctx: ServerRequestContext, params: RequestParams) -> Result: - assert ctx.method == "example/fail" - if failure == "mcp": - raise MCPError(code=12345, message="refused", data={"reason": "application"}) - if failure == "self-cancel": - raise anyio.get_cancelled_exc_class()() - IntegerValue.model_validate({"value": "not an integer"}) - raise NotImplementedError - - server = Server("failures") - server.add_request_handler("example/fail", RequestParams, handler) - with anyio.fail_after(5): - async with serving(server) as target, grpc.aio.insecure_channel(target) as channel: - async with Client(grpc_client(channel), mode="2026-07-28") as client: - with pytest.raises(MCPError) as exc: - await client.session.send_request(Request(method="example/fail", params=RequestParams()), Result) - assert ( - exc.value.code - == {"mcp": 12345, "validation": INVALID_PARAMS, "self-cancel": CONNECTION_CLOSED}[failure] - ) - - -@pytest.mark.anyio -async def test_null_parameters_reach_mcp_envelope_validation() -> None: - """Null is valid in the binding but lacks the MCP envelope, which the typed client normally always supplies.""" - with anyio.fail_after(5): - async with serving(Server("envelope")) as target, grpc.aio.insecure_channel(target) as channel: - call = channel.unary_stream( - "/mcp.transport.example.MCP/Call", - request_serializer=CallRequest.SerializeToString, - response_deserializer=CallEvent.FromString, - ) - events = [ - event - async for event in call(CallRequest(method="example/test", params_json=b"null", request_id_json=b"0")) - ] - assert len(events) == 1 - assert events[0].WhichOneof("payload") == "error_json" - assert json.loads(events[0].error_json)["code"] == INVALID_PARAMS - - -@pytest.mark.anyio -async def test_progress_callback_failure_does_not_abort_the_request(caplog: pytest.LogCaptureFixture) -> None: - """A client callback failure is isolated from the server result and logged with its traceback.""" - server = MCPServer("callback isolation") - - @server.tool() - async def ready(ctx: Context) -> str: - await ctx.report_progress(1, 2, "working") - return "ready" - - async def progress(progress: float, total: float | None, message: str | None) -> None: - raise RuntimeError("callback failed") - - with anyio.fail_after(5): - async with serving(server) as target, grpc.aio.insecure_channel(target) as channel: - async with Client(grpc_client(channel), mode="2026-07-28") as client: - result = await client.call_tool("ready", progress_callback=progress) - assert result.structured_content == {"result": "ready"} - records = [record for record in caplog.records if record.name == "mcp_transport_examples.grpc_response"] - assert len(records) == 1 - assert records[0].exc_info is not None - - -@pytest.mark.anyio -async def test_subscription_acknowledgment_and_events_use_the_original_rpc() -> None: - """A live listen RPC remains open while a separate tool request publishes a typed change event.""" - server = MCPServer("subscriptions") - - @server.tool() - async def announce(ctx: Context) -> str: - await ctx.notify_tools_changed() - return "announced" - - with anyio.fail_after(5): - async with serving(server) as target, grpc.aio.insecure_channel(target) as channel: - async with Client(grpc_client(channel), mode="2026-07-28") as client: - async with client.listen(tools_list_changed=True) as subscription: - result = await client.call_tool("announce") - assert result.structured_content == {"result": "announced"} - event = await anext(subscription) - assert isinstance(event, ToolsListChanged) - - -@pytest.mark.anyio -async def test_invalid_capacity_fails_before_registering_the_binding() -> None: - """Invalid configuration fails locally, without creating an RPC or binding a listening socket.""" - listener = grpc.aio.server() - with anyio.fail_after(5), pytest.raises(ValueError): - async with grpc_server(listener, max_requests=0).connection: - raise NotImplementedError diff --git a/examples/transports/tests/test_grpc_shutdown_order.py b/examples/transports/tests/test_grpc_shutdown_order.py deleted file mode 100644 index c2f934d235..0000000000 --- a/examples/transports/tests/test_grpc_shutdown_order.py +++ /dev/null @@ -1,93 +0,0 @@ -"""Lifespan resources must outlive a handler performing shielded cleanup.""" - -from collections.abc import AsyncIterator -from contextlib import asynccontextmanager - -import anyio -import anyio.abc -import grpc.aio -import pytest -from mcp import Client, MCPError -from mcp.server.mcpserver import MCPServer -from mcp.types import CONNECTION_CLOSED - -from mcp_transport_examples.grpc import grpc_client, grpc_server - - -async def verify() -> None: - entered = anyio.Event() - cleanup_started = anyio.Event() - release_cleanup = anyio.Event() - cleanup_finished = anyio.Event() - lifespan_closed = anyio.Event() - stop = anyio.Event() - - @asynccontextmanager - async def lifespan(server: MCPServer[None]) -> AsyncIterator[None]: - try: - yield None - finally: - lifespan_closed.set() - assert cleanup_finished.is_set() - - server = MCPServer("shutdown order", lifespan=lifespan) - - @server.tool() - async def wait() -> str: - entered.set() - try: - await anyio.sleep_forever() - finally: - with anyio.CancelScope(shield=True): - cleanup_started.set() - await release_cleanup.wait() - assert not lifespan_closed.is_set() - cleanup_finished.set() - raise NotImplementedError - - listener = grpc.aio.server() - port = listener.add_insecure_port("127.0.0.1:0") - - async def run_server(*, task_status: anyio.abc.TaskStatus[None]) -> None: - try: - async with server.serve() as runtime: - await runtime.connect(grpc_server(listener)) - await listener.start() - task_status.started() - await stop.wait() - finally: - with anyio.move_on_after(5, shield=True): - await listener.stop(0) - - async with anyio.create_task_group() as tg: - await tg.start(run_server) - async with grpc.aio.insecure_channel(f"127.0.0.1:{port}") as channel: - async with Client(grpc_client(channel), mode="2026-07-28") as client: - - async def call() -> None: - with pytest.raises(MCPError) as exc: - await client.call_tool("wait") - assert exc.value.code == CONNECTION_CLOSED - - async with anyio.create_task_group() as calls: - calls.start_soon(call) - try: - await entered.wait() - stop.set() - await cleanup_started.wait() - # The old five-second join timeout closed lifespan while this cleanup still ran. - with anyio.move_on_after(5.1) as window: - await lifespan_closed.wait() - assert window.cancelled_caught - finally: - release_cleanup.set() - await lifespan_closed.wait() - assert cleanup_finished.is_set() - - -@pytest.mark.anyio -async def test_runtime_keeps_lifespan_alive_through_shielded_handler_cleanup() -> None: - """The live handler must finish using application state before lifespan releases it.""" - # This check intentionally holds cleanup past the old five-second deadline. - with anyio.fail_after(10): - await verify() diff --git a/examples/transports/tests/test_grpc_tls.py b/examples/transports/tests/test_grpc_tls.py deleted file mode 100644 index ef66ec69ce..0000000000 --- a/examples/transports/tests/test_grpc_tls.py +++ /dev/null @@ -1,177 +0,0 @@ -import ipaddress -from contextlib import AsyncExitStack -from datetime import datetime, timedelta, timezone -from typing import Literal - -import anyio -import grpc -import grpc.aio -import pytest -from cryptography import x509 -from cryptography.hazmat.primitives import hashes, serialization -from cryptography.hazmat.primitives.asymmetric import ec -from cryptography.x509.oid import ExtendedKeyUsageOID, NameOID -from mcp import Client, MCPError -from mcp.server.context import CallNext, HandlerResult, ServerRequestContext -from mcp.server.mcpserver import Context, MCPServer -from mcp.types import ( - CLIENT_CAPABILITIES_META_KEY, - CLIENT_INFO_META_KEY, - CONNECTION_CLOSED, - PROTOCOL_VERSION_META_KEY, - CallToolRequestParams, - CallToolResult, - Implementation, -) - -from mcp_transport_examples.grpc import grpc_client, grpc_server -from mcp_transport_examples.grpc_context import GRPCContext -from mcp_transport_examples.rpc_pb2 import CallEvent, CallRequest - - -def certificate( - name: str, - key: ec.EllipticCurvePrivateKey, - issuer: x509.Name, - issuer_key: ec.EllipticCurvePrivateKey, - *, - ca: bool = False, - server: bool = False, -) -> bytes: - now = datetime.now(timezone.utc) - builder = ( - x509.CertificateBuilder() - .subject_name(x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, name)])) - .issuer_name(issuer) - .public_key(key.public_key()) - .serial_number(x509.random_serial_number()) - .not_valid_before(now - timedelta(days=1)) - .not_valid_after(now + timedelta(days=1)) - .add_extension(x509.BasicConstraints(ca=ca, path_length=None), critical=True) - ) - if not ca: - builder = builder.add_extension( - x509.ExtendedKeyUsage([ExtendedKeyUsageOID.SERVER_AUTH if server else ExtendedKeyUsageOID.CLIENT_AUTH]), - critical=False, - ) - if server: - builder = builder.add_extension( - x509.SubjectAlternativeName([x509.IPAddress(ipaddress.ip_address("127.0.0.1"))]), critical=False - ) - return builder.sign(issuer_key, hashes.SHA256()).public_bytes(serialization.Encoding.PEM) - - -def private_bytes(key: ec.EllipticCurvePrivateKey) -> bytes: - return key.private_bytes( - serialization.Encoding.PEM, serialization.PrivateFormat.PKCS8, serialization.NoEncryption() - ) - - -@pytest.mark.anyio -@pytest.mark.parametrize("security", ["insecure", "tls", "mtls", "missing-certificate", "untrusted-certificate"]) -async def test_peer_identity_comes_from_tls_not_caller_claims( - security: Literal["insecure", "tls", "mtls", "missing-certificate", "untrusted-certificate"], -) -> None: - """SDK-defined: native identity ignores caller claims; rejected TLS peers never reach middleware. - - Steps: 1. Make a typed client call. 2. Check authenticated or anonymous identity. - 3. Inject identity-looking RPC metadata, which the typed client cannot supply, and check identity again. - """ - root_key = ec.generate_private_key(ec.SECP256R1()) - root_name = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, "test root")]) - root_cert = certificate("test root", root_key, root_name, root_key, ca=True) - server_key = ec.generate_private_key(ec.SECP256R1()) - server_cert = certificate("test server", server_key, root_name, root_key, server=True) - client_key = ec.generate_private_key(ec.SECP256R1()) - issuer_key = ec.generate_private_key(ec.SECP256R1()) if security == "untrusted-certificate" else root_key - client_cert = certificate("alice", client_key, root_name, issuer_key) - client_info = Implementation(name="bob", version="1").model_dump(by_alias=True, exclude_none=True) - reached: list[str] = [] - claims: list[str | bytes | None] = [] - - async def observe(ctx: ServerRequestContext, call_next: CallNext) -> HandlerResult: - assert isinstance(ctx.transport, GRPCContext) - assert ctx.params is not None - assert ctx.params["_meta"][CLIENT_INFO_META_KEY] == client_info - reached.append(ctx.method) - claims.append(dict(ctx.transport.metadata).get("x509_common_name")) - return await call_next(ctx) - - server = MCPServer("TLS", middleware=[observe]) - - @server.tool() - async def identity(ctx: Context) -> dict[str, str | list[str] | None]: - assert ctx.request_context.method == "tools/call" - assert isinstance(ctx.transport, GRPCContext) - return { - "key": ctx.transport.peer_identity_key, - "identities": [value.decode("utf-8") for value in ctx.transport.peer_identities], - } - - with anyio.fail_after(5): - async with AsyncExitStack() as stack: - listener = grpc.aio.server() - credentials = grpc.ssl_server_credentials( - [(private_bytes(server_key), server_cert)], - root_certificates=root_cert, - require_client_auth=security != "tls", - ) - port = ( - listener.add_insecure_port("127.0.0.1:0") - if security == "insecure" - else listener.add_secure_port("127.0.0.1:0", credentials) - ) - stack.push_async_callback(listener.stop, 0) - runtime = await stack.enter_async_context(server.serve()) - await runtime.connect(grpc_server(listener)) - await listener.start() - present_certificate = security in ("mtls", "untrusted-certificate") - channel_credentials = grpc.ssl_channel_credentials( - root_certificates=root_cert, - private_key=private_bytes(client_key) if present_certificate else None, - certificate_chain=client_cert if present_certificate else None, - ) - channel = await stack.enter_async_context( - grpc.aio.insecure_channel(f"127.0.0.1:{port}") - if security == "insecure" - else grpc.aio.secure_channel(f"127.0.0.1:{port}", channel_credentials) - ) - client = await stack.enter_async_context( - Client(grpc_client(channel), mode="2026-07-28", client_info=Implementation(name="bob", version="1")) - ) - if security in ("missing-certificate", "untrusted-certificate"): - with pytest.raises(MCPError) as exc: - await client.call_tool("identity") - assert exc.value.code == CONNECTION_CLOSED - assert reached == [] - else: - result = await client.call_tool("identity") - assert result.structured_content == { - "key": "x509_common_name" if security == "mtls" else None, - "identities": ["alice"] if security == "mtls" else [], - } - assert "tools/call" in reached - assert all(claim is None for claim in claims) - rpc = channel.unary_stream( - "/mcp.transport.example.MCP/Call", - request_serializer=CallRequest.SerializeToString, - response_deserializer=CallEvent.FromString, - ) - params = CallToolRequestParams( - name="identity", - _meta={ - PROTOCOL_VERSION_META_KEY: "2026-07-28", - CLIENT_CAPABILITIES_META_KEY: {}, - CLIENT_INFO_META_KEY: client_info, - }, - ) - request = CallRequest( - method="tools/call", - params_json=params.model_dump_json(by_alias=True).encode("utf-8"), - request_id_json=b"1", - ) - events = [event async for event in rpc(request, metadata=(("x509_common_name", "mallory"),))] - assert len(events) == 1 - forged = CallToolResult.model_validate_json(events[0].result_json) - assert forged.structured_content == result.structured_content - assert claims[-1] == "mallory" diff --git a/examples/transports/tests/test_mqtt.py b/examples/transports/tests/test_mqtt.py deleted file mode 100644 index b4651e4708..0000000000 --- a/examples/transports/tests/test_mqtt.py +++ /dev/null @@ -1,28 +0,0 @@ -import aiomqtt -import pytest - -from mcp_transport_examples.mqtt import mqtt_transport - - -@pytest.mark.anyio -@pytest.mark.parametrize( - ("incoming", "outgoing", "expiry", "size"), - [ - ("same", "same", 60, 4096), - ("bad/#", "response", 60, 4096), - ("request", "bad/+", 60, 4096), - ("request", "response", 0, 4096), - ("request", "response", 2**32, 4096), - ("request", "response", 60, 0), - ], -) -async def test_invalid_configuration_fails_without_connecting( - incoming: str, outgoing: str, expiry: int, size: int -) -> None: - """Adapter-defined constraints reject unsafe topic routing and limits before touching a broker.""" - client = aiomqtt.Client("unused.invalid", protocol=aiomqtt.ProtocolVersion.V5) - with pytest.raises(ValueError): - async with mqtt_transport( - client, incoming_topic=incoming, outgoing_topic=outgoing, expiry=expiry, max_message_size=size - ): - raise NotImplementedError diff --git a/pyproject.toml b/pyproject.toml index c76b44655d..b2f26da55f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -245,7 +245,7 @@ max-returns = 13 # Default is 6 max-statements = 102 # Default is 50 [tool.uv.workspace] -members = ["src/mcp-types", "examples", "examples/clients/*", "examples/servers/*", "examples/snippets", "examples/transports"] +members = ["src/mcp-types", "examples", "examples/clients/*", "examples/servers/*", "examples/snippets"] [tool.uv.sources] mcp = { workspace = true } @@ -254,8 +254,6 @@ mcp-types = { workspace = true } strict-no-cover = { git = "https://github.com/pydantic/strict-no-cover" } [tool.pytest.ini_options] -# Live broker checks belong to the optional transport example package. -testpaths = ["tests"] log_cli = true xfail_strict = true # tests/docs/ imports the docs tooling, top-level modules under scripts/docs/. diff --git a/src/mcp/server/runner.py b/src/mcp/server/runner.py index 9f1657e470..6fb36b4521 100644 --- a/src/mcp/server/runner.py +++ b/src/mcp/server/runner.py @@ -655,7 +655,8 @@ async def serve_dual_era_loop( transport_builder=transport_builder, ) finally: - await write_stream.aclose() + with anyio.move_on_after(_EXIT_STACK_CLOSE_TIMEOUT, shield=True): + await write_stream.aclose() _PRE_REQUEST_REPLAY_LIMIT: int = 8 @@ -721,7 +722,8 @@ async def replay_then_relay() -> None: yield opening_request, replayed tg.cancel_scope.cancel() finally: - await read_stream.aclose() + with anyio.move_on_after(_EXIT_STACK_CLOSE_TIMEOUT, shield=True): + await read_stream.aclose() replay_send.close() replay_receive.close() diff --git a/src/mcp/server/sse.py b/src/mcp/server/sse.py index d71ef25004..7a35041d5e 100644 --- a/src/mcp/server/sse.py +++ b/src/mcp/server/sse.py @@ -59,6 +59,7 @@ async def handle_sse(request): ) from mcp.shared._context_streams import ContextSendStream, create_context_streams from mcp.shared.message import ServerMessageMetadata, SessionMessage +from mcp.shared.transport_context import TransportContext logger = logging.getLogger(__name__) @@ -279,7 +280,10 @@ async def _handle_post_message(self, scope: Scope, receive: Receive, send: Send) return # Pass the ASGI scope for framework-agnostic access to request data - metadata = ServerMessageMetadata(request_context=request) + metadata = ServerMessageMetadata( + request_context=request, + transport_context=TransportContext(kind="sse", can_send_request=True, headers=request.headers), + ) session_message = SessionMessage(message, metadata=metadata) logger.debug(f"Sending session message to writer: {session_message}") response = Response("Accepted", status_code=202) diff --git a/src/mcp/server/streamable_http.py b/src/mcp/server/streamable_http.py index 416dd9e2b4..b68fb97f5f 100644 --- a/src/mcp/server/streamable_http.py +++ b/src/mcp/server/streamable_http.py @@ -46,6 +46,7 @@ from mcp.shared._stream_protocols import ReadStream, WriteStream from mcp.shared.inbound import MCP_PROTOCOL_VERSION_HEADER from mcp.shared.message import CloseSSEStreamCallback, ServerMessageMetadata, SessionMessage +from mcp.shared.transport_context import TransportContext logger = logging.getLogger(__name__) @@ -252,6 +253,9 @@ def _message_metadata( close_standalone_sse_stream=close_standalone_sse_stream, on_request_unanswered=on_request_unanswered, can_send_request=not self.is_json_response_enabled, + transport_context=TransportContext( + kind="streamable-http", can_send_request=not self.is_json_response_enabled, headers=request.headers + ), ) def close_sse_stream(self, request_id: RequestId) -> None: diff --git a/src/mcp/server/streamable_http_manager.py b/src/mcp/server/streamable_http_manager.py index a7efac3fbc..0a9ae83886 100644 --- a/src/mcp/server/streamable_http_manager.py +++ b/src/mcp/server/streamable_http_manager.py @@ -6,6 +6,7 @@ import logging import math from collections.abc import AsyncIterator +from dataclasses import replace from typing import TYPE_CHECKING, Any, Final from uuid import uuid4 @@ -28,6 +29,7 @@ from mcp.shared._compat import resync_tracer from mcp.shared.inbound import MCP_PROTOCOL_VERSION_HEADER from mcp.shared.jsonrpc_dispatcher import JSONRPCDispatcher +from mcp.shared.message import MessageMetadata, ServerMessageMetadata from mcp.shared.transport_context import TransportContext if TYPE_CHECKING: @@ -216,6 +218,11 @@ async def _handle_stateless_request( security_settings=self.security_settings, ) + def transport_context(metadata: MessageMetadata) -> TransportContext: + assert isinstance(metadata, ServerMessageMetadata) + assert metadata.transport_context is not None + return replace(metadata.transport_context, can_send_request=False) + # Start server in a new task async def run_stateless_server(*, task_status: TaskStatus[None] = anyio.TASK_STATUS_IGNORED): async with http_transport.connect() as streams: @@ -230,7 +237,7 @@ async def run_stateless_server(*, task_status: TaskStatus[None] = anyio.TASK_STA # reply has nowhere to land — `can_send_request=False` # makes the per-request channel raise `NoBackChannelError` # for requests while still allowing notifications. - transport_builder=lambda _md: TransportContext(kind="streamable-http", can_send_request=False), + transport_builder=transport_context, ) # Born-ready, no standalone channel: the legacy stateless path # never opens a GET stream and need not see `initialize`. The diff --git a/src/mcp/shared/direct_dispatcher.py b/src/mcp/shared/direct_dispatcher.py index cbfaa7d0e2..b9e874eeac 100644 --- a/src/mcp/shared/direct_dispatcher.py +++ b/src/mcp/shared/direct_dispatcher.py @@ -226,7 +226,7 @@ async def _operation(self, peer: DirectDispatcher) -> AsyncIterator[None]: self._operations.pop(scope) peer._operations.pop(scope, None) finished.set() - if scope.cancelled_caught: + if scope.cancel_called: raise _DispatchClosed def _make_context( diff --git a/src/mcp/shared/dispatcher.py b/src/mcp/shared/dispatcher.py index e8d113e424..15f0d4b3eb 100644 --- a/src/mcp/shared/dispatcher.py +++ b/src/mcp/shared/dispatcher.py @@ -211,9 +211,10 @@ def cancel_requested(self) -> anyio.Event: ... async def progress(self, progress: float, total: float | None = None, message: str | None = None) -> None: - """Report progress for the inbound request, if the peer supplied a progress token. + """Report progress for the inbound request when the peer opted in. - A no-op when no token was supplied. + JSON-RPC uses a progress token; direct and native bindings can carry + the callback opt-in separately. Without an opt-in this is a no-op. """ ... diff --git a/src/mcp/shared/jsonrpc_dispatcher.py b/src/mcp/shared/jsonrpc_dispatcher.py index 87bdf31ceb..f4b41b4ea9 100644 --- a/src/mcp/shared/jsonrpc_dispatcher.py +++ b/src/mcp/shared/jsonrpc_dispatcher.py @@ -10,7 +10,7 @@ import contextvars import logging from collections.abc import Awaitable, Callable, Mapping -from dataclasses import dataclass, field +from dataclasses import dataclass, field, replace from functools import partial from typing import Any, Generic, Literal, cast @@ -194,6 +194,8 @@ def _default_transport_builder(metadata: MessageMetadata) -> TransportContext: its streams. """ can_send_request = metadata.can_send_request if isinstance(metadata, ServerMessageMetadata) else True + if isinstance(metadata, ServerMessageMetadata) and metadata.transport_context is not None: + return replace(metadata.transport_context, can_send_request=can_send_request) return TransportContext(kind="jsonrpc", can_send_request=can_send_request) diff --git a/src/mcp/shared/message.py b/src/mcp/shared/message.py index 31e51e7128..ed34072626 100644 --- a/src/mcp/shared/message.py +++ b/src/mcp/shared/message.py @@ -5,11 +5,13 @@ """ from collections.abc import Awaitable, Callable -from dataclasses import dataclass +from dataclasses import dataclass, field from typing import Any from mcp_types import JSONRPCMessage, RequestId +from mcp.shared.transport_context import TransportContext + ResumptionToken = str ResumptionTokenUpdateCallback = Callable[[ResumptionToken], Awaitable[None]] @@ -50,6 +52,8 @@ class ServerMessageMetadata: # `TransportContext.can_send_request`); a transport that says nothing leaves # it True. can_send_request: bool = True + transport_context: TransportContext | None = field(default=None, kw_only=True, repr=False) + """Context supplied by the framing transport; omitted from repr to avoid logging request headers.""" MessageMetadata = ClientMessageMetadata | ServerMessageMetadata | None diff --git a/tests/docs_src/test_authorization.py b/tests/docs_src/test_authorization.py index 00c9adc81c..1da83f9e16 100644 --- a/tests/docs_src/test_authorization.py +++ b/tests/docs_src/test_authorization.py @@ -1,20 +1,88 @@ """`docs/run/authorization.md`: every claim the page makes, proved against the real SDK.""" +from collections.abc import AsyncIterator +from contextlib import asynccontextmanager + +import anyio import httpx2 import pytest from inline_snapshot import snapshot -from mcp_types import TextContent +from mcp_types import ( + INVALID_PARAMS, + ElicitRequest, + ElicitRequestFormParams, + ElicitResult, + InputRequiredResult, + InputResponses, + TextContent, +) from starlette.routing import Route -from docs_src.authorization import tutorial001, tutorial002 -from mcp import Client +from docs_src.authorization import tutorial001, tutorial002, tutorial003 +from mcp import Client, MCPError from mcp.client.streamable_http import streamable_http_client from mcp.server import MCPServer +from mcp.server.mcpserver import Context +from mcp.shared.memory import create_client_server_memory_streams +from mcp.shared.transport import MessageMetadata, TransportContext, TransportStreams # See test_index.py for why this is a per-module mark and not a conftest hook. pytestmark = [pytest.mark.anyio, pytest.mark.filterwarnings("error::mcp.MCPDeprecationWarning")] +async def test_verified_transport_identity_binds_the_published_request_state_example() -> None: + """tutorial003: issued state accepts its verified peer and rejects an anonymous retry through the public runtime.""" + + @tutorial003.mcp.tool() + async def confirm(ctx: Context) -> str | InputRequiredResult: + if ctx.input_responses is not None: + assert isinstance(ctx.request_state, str) + return ctx.request_state + return InputRequiredResult( + input_requests={ + "confirm": ElicitRequest( + params=ElicitRequestFormParams( + message="Confirm?", + requested_schema={"type": "object", "properties": {}}, + ) + ) + }, + request_state="approved", + ) + + with anyio.fail_after(5): + async with tutorial003.mcp.serve() as runtime: + + @asynccontextmanager + async def connection(verified: bool) -> AsyncIterator[TransportStreams]: + async with create_client_server_memory_streams() as (client_streams, server_streams): + + def builder(metadata: MessageMetadata) -> TransportContext: + if verified: + return tutorial003.VerifiedPeer(kind="broker", can_send_request=False, principal="alice") + return TransportContext(kind="broker", can_send_request=False) + + @asynccontextmanager + async def transport() -> AsyncIterator[TransportStreams]: + yield server_streams + + await runtime.connect(transport(), transport_builder=builder) + yield client_streams + + async with Client(connection(True)) as alice, Client(connection(False)) as anonymous: + pending = await alice.session.call_tool("confirm", allow_input_required=True) + assert isinstance(pending, InputRequiredResult) + assert pending.request_state is not None + responses: InputResponses = {"confirm": ElicitResult(action="accept")} + with pytest.raises(MCPError) as exc: + await anonymous.call_tool("confirm", input_responses=responses, request_state=pending.request_state) + assert exc.value.code == INVALID_PARAMS + result = await alice.call_tool( + "confirm", input_responses=responses, request_state=pending.request_state + ) + assert result.structured_content == {"result": "approved"} + + async def test_the_in_memory_client_never_authenticates() -> None: """tutorial001: `Client(mcp)` connects to the server object directly, so no token is ever checked.""" async with Client(tutorial001.mcp) as client: diff --git a/tests/server/test_runtime.py b/tests/server/test_runtime.py index 466f8d6b13..a9fa9e7d96 100644 --- a/tests/server/test_runtime.py +++ b/tests/server/test_runtime.py @@ -38,6 +38,7 @@ from mcp.shared.transport import ( DispatcherTransport, MessageMetadata, + SessionMessage, TransportContext, TransportContextBuilder, TransportStreams, @@ -158,7 +159,7 @@ async def inspect_context(ctx: Context) -> dict[str, str | bool]: assert metadata.can_send_request is True -async def test_host_releases_transport_before_application_lifespan() -> None: +async def test_host_releases_transport_before_application_lifespan(monkeypatch: pytest.MonkeyPatch) -> None: """Host exit cancels a connected peer and lets its adapter clean up before the application does.""" events: list[str] = [] @@ -170,7 +171,24 @@ async def lifespan(server: Server[None]) -> AsyncIterator[None]: await anyio.lowlevel.checkpoint() events.append("lifespan") - async with create_client_server_memory_streams() as (client_streams, server_streams): + request_send, request_receive = anyio.create_memory_object_stream[SessionMessage | Exception]() + response_send, response_receive = anyio.create_memory_object_stream[SessionMessage]() + async with request_send, request_receive, response_send, response_receive: + server_streams = request_receive, response_send + read_close, write_close = request_receive.aclose, response_send.aclose + + async def close_read() -> None: + await anyio.lowlevel.checkpoint() + events.append("read") + await read_close() + + async def close_write() -> None: + await anyio.lowlevel.checkpoint() + events.append("write") + await write_close() + + monkeypatch.setattr(server_streams[0], "aclose", close_read) + monkeypatch.setattr(server_streams[1], "aclose", close_write) @asynccontextmanager async def transport() -> AsyncIterator[TransportStreams]: @@ -183,9 +201,9 @@ async def transport() -> AsyncIterator[TransportStreams]: with anyio.fail_after(5): async with Server("shutdown", lifespan=lifespan).serve() as host: await host.connect(transport()) - assert events == ["transport", "lifespan"] + assert events == ["read", "write", "transport", "lifespan"] with pytest.raises(anyio.EndOfStream): - await client_streams[0].receive() + await response_receive.receive() async def test_host_survives_an_adapter_failure_after_opening(caplog: pytest.LogCaptureFixture) -> None: diff --git a/tests/shared/test_dispatcher.py b/tests/shared/test_dispatcher.py index cf7232baa2..b2ba3e1848 100644 --- a/tests/shared/test_dispatcher.py +++ b/tests/shared/test_dispatcher.py @@ -577,7 +577,12 @@ def broken_intercept(method: str, params: Mapping[str, Any] | None) -> bool: @pytest.mark.anyio @pytest.mark.parametrize("closing_side", ["client", "server"]) @pytest.mark.parametrize("operation", ["request", "notification"]) -async def test_direct_close_joins_in_flight_handler_cleanup(closing_side: str, operation: str) -> None: +@pytest.mark.parametrize("swallow_cancel", [False, True]) +async def test_direct_close_joins_in_flight_handler_cleanup( + closing_side: str, + operation: str, + swallow_cancel: bool, +) -> None: """Closing either peer interrupts its conversation and keeps run alive until shielded handler cleanup finishes.""" entered = anyio.Event() cleaning = anyio.Event() @@ -590,6 +595,9 @@ async def handle() -> None: entered.set() try: await anyio.sleep_forever() + except anyio.get_cancelled_exc_class(): + if not swallow_cancel: + raise finally: with anyio.CancelScope(shield=True): cleaning.set() @@ -601,7 +609,7 @@ async def request( ) -> dict[str, Any]: assert method == "work" await handle() - raise NotImplementedError + return {} async def notify(ctx: DispatchContext[TransportContext], method: str, params: Mapping[str, Any] | None) -> None: assert method == "work" diff --git a/tests/shared/test_message.py b/tests/shared/test_message.py new file mode 100644 index 0000000000..8b6ac0b74a --- /dev/null +++ b/tests/shared/test_message.py @@ -0,0 +1,14 @@ +from mcp.shared.transport import ServerMessageMetadata, SessionMessage, TransportContext +from mcp.types import JSONRPCRequest + + +def test_transport_headers_are_not_exposed_by_message_representations() -> None: + """SDK-defined: framing metadata preserves header access without adding credentials to debug representations.""" + credential = "private-bearer-value" + context = TransportContext(kind="http", can_send_request=False, headers={"authorization": credential}) + metadata = ServerMessageMetadata(None, None, None, None, None, False, transport_context=context) + message = SessionMessage(JSONRPCRequest(jsonrpc="2.0", id=1, method="ping"), metadata) + assert metadata.transport_context is context + assert not metadata.can_send_request + assert credential not in repr(metadata) + assert credential not in repr(message) diff --git a/tests/shared/test_sse.py b/tests/shared/test_sse.py index 77d1b28a0a..f1b28c7848 100644 --- a/tests/shared/test_sse.py +++ b/tests/shared/test_sse.py @@ -241,7 +241,11 @@ async def test_sse_client_basic_connection_mounted_app() -> None: async def _handle_context_call_tool(ctx: ServerRequestContext, params: CallToolRequestParams) -> CallToolResult: assert params.name in ("echo_headers", "echo_context") assert ctx.request is not None - headers_info = dict(ctx.request.headers) + assert ctx.transport is not None + assert ctx.transport.kind == "sse" + assert ctx.transport.headers == ctx.request.headers + assert ctx.transport.headers is not None + headers_info = dict(ctx.transport.headers) if params.name == "echo_headers": return CallToolResult(content=[TextContent(type="text", text=json.dumps(headers_info))]) diff --git a/tests/shared/test_streamable_http.py b/tests/shared/test_streamable_http.py index 655d9941dc..e0d2e96c5f 100644 --- a/tests/shared/test_streamable_http.py +++ b/tests/shared/test_streamable_http.py @@ -1410,14 +1410,18 @@ async def _handle_context_call_tool( ) -> CallToolResult: assert params.name in ("echo_headers", "echo_context") assert isinstance(ctx.request, Request) + assert ctx.transport is not None + assert ctx.transport.kind == "streamable-http" + assert ctx.transport.headers == ctx.request.headers + assert ctx.transport.headers is not None if params.name == "echo_headers": - return CallToolResult(content=[TextContent(type="text", text=json.dumps(dict(ctx.request.headers)))]) + return CallToolResult(content=[TextContent(type="text", text=json.dumps(dict(ctx.transport.headers)))]) assert params.arguments is not None context_data: dict[str, Any] = { "request_id": params.arguments.get("request_id"), - "headers": dict(ctx.request.headers), + "headers": dict(ctx.transport.headers), "method": ctx.request.method, "path": ctx.request.url.path, "protocol_version": ctx.protocol_version, diff --git a/uv.lock b/uv.lock index b6cad20455..40b563e974 100644 --- a/uv.lock +++ b/uv.lock @@ -3,8 +3,7 @@ revision = 3 requires-python = ">=3.10" resolution-markers = [ "python_full_version >= '3.14'", - "python_full_version >= '3.11' and python_full_version < '3.14'", - "python_full_version < '3.11'", + "python_full_version < '3.14'", ] [manifest] @@ -25,7 +24,6 @@ members = [ "mcp-sse-polling-client", "mcp-sse-polling-demo", "mcp-structured-output-lowlevel", - "mcp-transport-examples", "mcp-types", ] build-constraints = [ @@ -42,86 +40,6 @@ build-constraints = [ { name = "uv-dynamic-versioning", specifier = "==0.14.0" }, ] -[[package]] -name = "aio-pika" -version = "9.6.2" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.11'", -] -dependencies = [ - { name = "aiormq", version = "6.9.4", source = { registry = "https://pypi.org/simple" } }, - { name = "exceptiongroup" }, - { name = "yarl" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/96/63/56354526f2e6e915c93bee6e4dedb35888fe82d6bc1a19f35f5a77e795ff/aio_pika-9.6.2.tar.gz", hash = "sha256:c49e9246080dc8ffa1bb0e4aca407bf3d8ad78c3ee3a93df88b68fe65d7a49b9", size = 70851, upload-time = "2026-03-22T19:03:20.878Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/25/05/256fa313f48bed075056d13593b92ce804be05d75f4f312be24edb82860a/aio_pika-9.6.2-py3-none-any.whl", hash = "sha256:2a5478af920d169795071c9c09c7542cd8cdece60438cf7804533dcbcce93b7f", size = 56269, upload-time = "2026-03-22T19:03:19.558Z" }, -] - -[[package]] -name = "aio-pika" -version = "10.0.1" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.14'", - "python_full_version >= '3.11' and python_full_version < '3.14'", -] -dependencies = [ - { name = "aiormq", version = "7.0.0", source = { registry = "https://pypi.org/simple" } }, - { name = "yarl" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/8f/7f/01f4ea7fe3490194420bb52e596b9619092ed13c5a230014b02075c3bd77/aio_pika-10.0.1.tar.gz", hash = "sha256:96ec3ef748ca7a25a9d2fa6e511c16c3ffcfa6b1f40ade79b8a5baabba682efd", size = 70882, upload-time = "2026-07-09T13:31:35.709Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/65/3f/329d0e52f994349ff7449c714c242ad65f14586b0e205ca632ac817fda72/aio_pika-10.0.1-py3-none-any.whl", hash = "sha256:12120a3cf8022d2a8bc5dc89e716512a38bf742c24c5562f54764af27eec7edd", size = 56332, upload-time = "2026-07-09T13:31:33.634Z" }, -] - -[[package]] -name = "aiomqtt" -version = "2.5.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "paho-mqtt" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/70/44/cfc58272783a11729462dc6df5adbfeabd084f840f609054ac772ae98c19/aiomqtt-2.5.1.tar.gz", hash = "sha256:25a0a47d157e8f158d2da1110ea4786c0615518751e94f7b04976c977a8ff20d", size = 86641, upload-time = "2026-03-05T18:28:56.421Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/01/9e/5089fa596220bf0dc73deeb23db27904e4b3504986caf08571f6f5cb84a8/aiomqtt-2.5.1-py3-none-any.whl", hash = "sha256:fd58c3593160e4d475d90ce911cdfc4239cd64de96b0ba22edf6c86bd7afa278", size = 16051, upload-time = "2026-03-05T18:28:55.14Z" }, -] - -[[package]] -name = "aiormq" -version = "6.9.4" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.11'", -] -dependencies = [ - { name = "pamqp", version = "3.3.0", source = { registry = "https://pypi.org/simple" } }, - { name = "yarl" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/6c/0e/db90154d52d399108903fe603e5110a533c42065180265dd003788264080/aiormq-6.9.4.tar.gz", hash = "sha256:0e7c01b662804e1cc7ace9a17794e8c1192a27fc2afa96162362a6e61ae8e8ef", size = 49232, upload-time = "2026-03-23T09:18:19.493Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6c/48/1ce3773f392f02ceda37aee168fade9d725483a9592c202d06044cd093ff/aiormq-6.9.4-py3-none-any.whl", hash = "sha256:726a8586695e863fba68cf88842065ab12348c9438dcebdfc9d0bddaf6083277", size = 32166, upload-time = "2026-03-23T09:18:17.523Z" }, -] - -[[package]] -name = "aiormq" -version = "7.0.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.14'", - "python_full_version >= '3.11' and python_full_version < '3.14'", -] -dependencies = [ - { name = "pamqp", version = "4.0.1", source = { registry = "https://pypi.org/simple" } }, - { name = "yarl" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/00/16/7e1c2bb887db6cbad191db9a1562e1cf5c0c61ad93f194ddc7baf5661f02/aiormq-7.0.0.tar.gz", hash = "sha256:f524121f1afbb875f50235b2748f81331e3be47542ee600e83c321c4e97ea168", size = 49231, upload-time = "2026-07-09T11:40:51.775Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/53/88/8da3627882f6bd75f780f87e46d0b58da99332c1b71d038db7a127a80648/aiormq-7.0.0-py3-none-any.whl", hash = "sha256:df49bb2282e5374a28507c4c43948e8c8e5321590f2998781c2d90a34e100789", size = 32152, upload-time = "2026-07-09T11:40:50.508Z" }, -] - [[package]] name = "annotated-types" version = "0.7.0" @@ -248,61 +166,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/63/c5/092e631bc1fba86f0a822be65c137c90a71b71ba0a0865e7e9a21f6ca05e/blockbuster-1.5.27-py3-none-any.whl", hash = "sha256:f0acf153d22a791bf5f142935332ef8530960ec215541b48a6037e6cea0a8645", size = 13517, upload-time = "2026-08-17T23:53:14.625Z" }, ] -[[package]] -name = "cassetter" -version = "0.11.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/51/8f/e934ce0b21f7045181b786412b5326d6f89b4f7f58303d4460054bc93872/cassetter-0.11.0.tar.gz", hash = "sha256:7578203459c08623f8f27e6e9179774359ceb101b9f207308274fdaae9e4d589", size = 102607, upload-time = "2026-09-10T12:06:54.626Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/be/5f/cb2d42df62d26b10c8f9e0ec227c08e992eff64f3a16b709f4823fda8fce/cassetter-0.11.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:af6aab0015cce3eadeebec7fbb15612c457aa482664170f6c7f7966fdd03b8ea", size = 1980907, upload-time = "2026-09-10T12:05:48.859Z" }, - { url = "https://files.pythonhosted.org/packages/31/45/25563caa90b94766a0bcd1473444aeccdddf6ee682e789fd3a24286eb5b2/cassetter-0.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ae0ba29a86fd290c2b0e19f39c598ec9176544d005de53a40a0c7a2eec332a49", size = 1857688, upload-time = "2026-09-10T12:05:50.531Z" }, - { url = "https://files.pythonhosted.org/packages/45/23/03e1902699ba6c9d6c849a50dacfe6e6a6937ce08589c0ee95b785a58b65/cassetter-0.11.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ff5729b084a201c0ff789272846b4876f4bd008b47d0929ce233727ac13fbdf", size = 1926524, upload-time = "2026-09-10T12:05:52.058Z" }, - { url = "https://files.pythonhosted.org/packages/f7/fe/9bc4776fb99ed57878e790c15c76e46532041cecc8d3587ae1537647a0cb/cassetter-0.11.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d5b9bfdeae7179ae79560180c5938754722d74b12b1c6f5a5845166da0ed2c5", size = 2070007, upload-time = "2026-09-10T12:05:53.701Z" }, - { url = "https://files.pythonhosted.org/packages/34/f0/405f4eabd04c122074618883a6810bbbe30f3e45dbc8da367b38390aa906/cassetter-0.11.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7c20f1a62a9394a9ddf2b09966a45698aa7d231a5016e6409cdace6d8157a019", size = 2112086, upload-time = "2026-09-10T12:05:55.421Z" }, - { url = "https://files.pythonhosted.org/packages/98/fc/bbfda42e4756945d9a4796bb37b7852c90248dbc73a7812f34f64239c00a/cassetter-0.11.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8feab7f50cd9ef59860f3d68883a8cc729ca780589d3e1ab6bee5da20e316c9a", size = 2297899, upload-time = "2026-09-10T12:05:57.111Z" }, - { url = "https://files.pythonhosted.org/packages/29/e7/74aa131e5d63d9b96e2e5d83bad908257e0d808668132bb41428f3b36784/cassetter-0.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:136964f2486aa8dad45e39517c4b43ad7faee30cf104f16736669d72ece73c4d", size = 2019629, upload-time = "2026-09-10T12:05:58.496Z" }, - { url = "https://files.pythonhosted.org/packages/d8/e3/e2be9ba63fa2720541e931332b31c7af2874caee2d368b4d8ed277ade158/cassetter-0.11.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:421178c75b31101c8c0af6b9b36a3ecdb55858dadc7767e498e9f7791a6d9f95", size = 1980953, upload-time = "2026-09-10T12:05:59.97Z" }, - { url = "https://files.pythonhosted.org/packages/36/de/ebe6dfc0d32c9b033f154a7a336d66eb93f4456d7335340e621b75129244/cassetter-0.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:52db9ee46971ffb68c3cf0df384dec15f6026df4352305ebe0bdf603d0532ee3", size = 1857850, upload-time = "2026-09-10T12:06:01.383Z" }, - { url = "https://files.pythonhosted.org/packages/79/49/b9f8f250c3d5817f165659561a5a007f6e3cf8e3984f3275ab6c8ca69c21/cassetter-0.11.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37de99fc2e71dd0b6231fb6087b363bf8c244ba5a095a0166768e14c233c7511", size = 1926553, upload-time = "2026-09-10T12:06:02.699Z" }, - { url = "https://files.pythonhosted.org/packages/87/c3/88f91278457a07a8186d65cf9aae9166ec1eb8a939ca18e23a9d9294b06c/cassetter-0.11.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5a2a77aaf2adb80a0f5c7860536d423d0e10fb579a3703f64e27eaf71111e75", size = 2070025, upload-time = "2026-09-10T12:06:04.069Z" }, - { url = "https://files.pythonhosted.org/packages/4d/7f/fc850ebdaad4467f103ae9d5939295be812447e0c6a72de15378e3663615/cassetter-0.11.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e39eab159142285b7b743a5ad64046bc4e3c38dba0383797737e45ecc3323b4f", size = 2111806, upload-time = "2026-09-10T12:06:05.7Z" }, - { url = "https://files.pythonhosted.org/packages/17/dd/94f3b7f6119878a45e8d193f64aead700776dd98183880f0e8067db38671/cassetter-0.11.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b03bed6a5842a26347d32bcfb0c1f4a87cc81ef2324e8d7677584036fa676706", size = 2297752, upload-time = "2026-09-10T12:06:07.519Z" }, - { url = "https://files.pythonhosted.org/packages/35/3b/6f7688fc36c40b20d6c2c6e81373117fa79cd4e705bae5ea98d488919180/cassetter-0.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:498eb6718c401746e432e365ba322920351bbb2f82260956b30b29e3b5a82ba4", size = 2019574, upload-time = "2026-09-10T12:06:09.599Z" }, - { url = "https://files.pythonhosted.org/packages/12/96/efe27ef22c195539e76c079391a39aff0b9791b98881cefc8c2680ff89e9/cassetter-0.11.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0e37ba7df95d7132a109d126889955541176902a830ac386a6061b716efc74f1", size = 1992016, upload-time = "2026-09-10T12:06:11.186Z" }, - { url = "https://files.pythonhosted.org/packages/46/82/7ca66ce3d58069692ce948437940fc657954c0047d63c57e8d1ebe21bc96/cassetter-0.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e035f81fc4096ba809b33d7dadf56233d8b77b6738f7458e52937fbc796d92aa", size = 1852366, upload-time = "2026-09-10T12:06:12.696Z" }, - { url = "https://files.pythonhosted.org/packages/b5/cc/8375f65d6f1385194570097c45018a01655023e35fbe457df09cfb61f492/cassetter-0.11.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c905cbd42dd322ed67bf2127c8b7b23ace222232f935427b79b6b902f470aff", size = 1924201, upload-time = "2026-09-10T12:06:14.283Z" }, - { url = "https://files.pythonhosted.org/packages/db/6a/d4094c2cd57d2f180e61cab004c6f78d3563f70002722e2aa0fbfc566215/cassetter-0.11.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a4c36109d40ceadb60024a011413efc9ab9f71ffb42fcd2c37bb8bb4dcf82d13", size = 2069606, upload-time = "2026-09-10T12:06:15.681Z" }, - { url = "https://files.pythonhosted.org/packages/df/04/c5b3e374d92427ed5a124a418a5e31e7bdb52932d9ca65fbe89d90bedc05/cassetter-0.11.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7b3086a2c180d854562c3ae2d455d114751c90783db81ccf83f63d1bed699722", size = 2110256, upload-time = "2026-09-10T12:06:16.996Z" }, - { url = "https://files.pythonhosted.org/packages/b7/33/7daa996190ac39686021d6df7d5c734e6762d8e444c77e29b5702fff4353/cassetter-0.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9c605d865c07b2bcb328cbf1d380d543a1703f1384a279fdae853e092d437cab", size = 2297399, upload-time = "2026-09-10T12:06:18.398Z" }, - { url = "https://files.pythonhosted.org/packages/d9/33/c0245b72c375c7e08ea4528ad53078b7b21053b1f665d7cf20252f131930/cassetter-0.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:c2f2967c91d87af8798c2f4ec88dd2be7b190b34d3ba20732bf612ebc35faf93", size = 2016727, upload-time = "2026-09-10T12:06:19.691Z" }, - { url = "https://files.pythonhosted.org/packages/a0/60/571036d38c0c98109200459ba2deb7bfe3db2354f481c8b9f559d5a99c71/cassetter-0.11.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:074242e9febdfdc8733d8b1ee3635d80af17c0ce68e501249b09c113ce6cdeee", size = 1992011, upload-time = "2026-09-10T12:06:21.247Z" }, - { url = "https://files.pythonhosted.org/packages/ae/ce/3f7d5d52e183eab67da5dad1403bd7ca3e27b4df179dec07439a6e1479b3/cassetter-0.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7a65cd317af3ebe7eb329e178bccabc56ce06cc6c40e023fc34bcbf5a5ff317e", size = 1852584, upload-time = "2026-09-10T12:06:22.691Z" }, - { url = "https://files.pythonhosted.org/packages/86/9a/363567977feb6797d394bd5267057f7db5e67e6d9a615150f104b70cd9a6/cassetter-0.11.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0225a7264649a1db81d81e86f779e3d0fcc189006109f446bdfd6bb31f890464", size = 1924781, upload-time = "2026-09-10T12:06:24.08Z" }, - { url = "https://files.pythonhosted.org/packages/c9/ae/8b910cb80abe925494f4dcb40cd61bf66ba1fb96f98d45bbd21860cc64db/cassetter-0.11.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0e40aff3a75ed10d309dc4e05d750bfa1873c4a6d8efd57eb1027b1fd415072", size = 2069427, upload-time = "2026-09-10T12:06:26.114Z" }, - { url = "https://files.pythonhosted.org/packages/76/fa/9dad53bf7b94add575e936d985998c18e4445d6d826426841e1084b2df13/cassetter-0.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:70803d4a3f3f0b631c648fc0c07cde2947b91f9694345a7a5172d0a60567d51c", size = 2109980, upload-time = "2026-09-10T12:06:27.805Z" }, - { url = "https://files.pythonhosted.org/packages/8f/ab/b15e1a3ce73a4cbef98a101940c344a681716a9cdf348ed2bdf0b8f0ede6/cassetter-0.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3f6a1ccb97b70a3db83ffddf1fab5d6e2978f2837d800acc08c2079a57dfef2b", size = 2297450, upload-time = "2026-09-10T12:06:29.524Z" }, - { url = "https://files.pythonhosted.org/packages/bd/cd/0c3b62801c1787c345e23168f8b08383b09fe612a5df3559a14522479ac3/cassetter-0.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:6bef0417a2babf8dd5b39a3187466bf647591d2bbef6a0db57d5717edd23d911", size = 2016955, upload-time = "2026-09-10T12:06:31.146Z" }, - { url = "https://files.pythonhosted.org/packages/38/11/cc8e13ff2446653a609faed97aca79329c0164033286167dcb1eee740f3c/cassetter-0.11.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:ace65b75f1f79a57ebf86037521a8c6050393a1cf933222405f6effe7ca6ff39", size = 1993017, upload-time = "2026-09-10T12:06:32.543Z" }, - { url = "https://files.pythonhosted.org/packages/35/25/79d95436827031edb0cd5fec82536f0765079cd4f20bb55f7021ea1eb293/cassetter-0.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:86196ca7af873231a8339616e11ae0b6159657e22c7b148c152d0998c2461a7b", size = 1854089, upload-time = "2026-09-10T12:06:34.197Z" }, - { url = "https://files.pythonhosted.org/packages/ff/40/0dac2e31cde85601fb6c757a4398fddb04dc816f5f807bcd829ea0f62ef0/cassetter-0.11.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:002b8bb2a9cb3250a9ad9c751278d32500a74548fefa5759f475eee86451d3d3", size = 1925672, upload-time = "2026-09-10T12:06:35.745Z" }, - { url = "https://files.pythonhosted.org/packages/d6/7f/e78e26fd90df1caef9070ffb74ad92e34390aa5e2a08160b836c22fefdbe/cassetter-0.11.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f70b6a46a8d89731a40baa6ca5042d944aeb201d89a56318f2c647b24de09bad", size = 2070668, upload-time = "2026-09-10T12:06:37.46Z" }, - { url = "https://files.pythonhosted.org/packages/24/01/763366bc70c0394b3b677648e0a217b7b017bc52ba51365eef0e8005d32e/cassetter-0.11.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:13214fc29329b21202259848cc85522ff1482e582fddd76683bac884afdf8545", size = 2110732, upload-time = "2026-09-10T12:06:38.903Z" }, - { url = "https://files.pythonhosted.org/packages/37/5f/ad7745cea2df68936942da76df3c176a0a581378c6171db91c2ed6a88271/cassetter-0.11.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7808afd0a4312b6b23b5befd2a1b50b22e5575bfda0340d13249e38200ef9a35", size = 2298322, upload-time = "2026-09-10T12:06:40.866Z" }, - { url = "https://files.pythonhosted.org/packages/89/57/383494395c4a4bdc27d9bcfb03e0c7fabd9cf2e8fdbc573d280c0352db38/cassetter-0.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:071c31f1a96de84cd8c6f699c6da19a2b38cf74d27bfa61d051327db1e994e8e", size = 2017296, upload-time = "2026-09-10T12:06:42.284Z" }, - { url = "https://files.pythonhosted.org/packages/b0/33/d100a48e7fcd065b80aca95eadf28531dcd6c686f818cf99f3e402a5ed85/cassetter-0.11.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:3872fba7fde12ccc860aff7d13cc3e6aefdc4dbe65bf6c9de8b73dcf8330e0a5", size = 1988047, upload-time = "2026-09-10T12:06:43.743Z" }, - { url = "https://files.pythonhosted.org/packages/b6/de/c2c175b5c09a1143cbd472d3270a464836d7c9b0b209f64cac8f2402f258/cassetter-0.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6c602d42f992d95c9eea42171bc7844aca0531c94e385ae70afc218d7abc8ae1", size = 1848022, upload-time = "2026-09-10T12:06:45.227Z" }, - { url = "https://files.pythonhosted.org/packages/e1/86/34936390872c5f5f24f09a2d96df878942807905e3b6a2551da136572d8f/cassetter-0.11.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:467562e1d0b8a19ee0eebb294397ea0973a064d1143bbc987d716ec83ffc2a69", size = 1918666, upload-time = "2026-09-10T12:06:46.77Z" }, - { url = "https://files.pythonhosted.org/packages/54/bb/cdc849e32bf111e7e5613554f1b6b5b3eb9e8133043d3ec3ac5c1260da0a/cassetter-0.11.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:57020ab03f7cda137310081628af2336347c0be3f64709455570f5bfebc4f411", size = 2065273, upload-time = "2026-09-10T12:06:48.47Z" }, - { url = "https://files.pythonhosted.org/packages/79/66/3740f408774d3c9f7d950d62a968530b468aba7320945ebdc931e94f97e1/cassetter-0.11.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:aedd8c76cc4ca3302c8ed4953b82737b64479e5d15c80d186e5051f5aaa3fea9", size = 2104326, upload-time = "2026-09-10T12:06:50.001Z" }, - { url = "https://files.pythonhosted.org/packages/59/a4/9b22abd9365e1e6f1b0208882b19a606347158e7c7e61fd21e25561e5a0a/cassetter-0.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:72fd743caadef19b0ddeac453fa1acff62c746998e81656706654a5f51cb1048", size = 2294473, upload-time = "2026-09-10T12:06:51.516Z" }, - { url = "https://files.pythonhosted.org/packages/47/10/634e5d897e03beaf808978c337d2b80dc02905570afce0cfec0d71b99910/cassetter-0.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:18997a61a6da1598d348506502849d5a3129727715d8eed599b8b451789b432b", size = 2016101, upload-time = "2026-09-10T12:06:53.313Z" }, -] - -[package.optional-dependencies] -grpc = [ - { name = "grpcio" }, -] - [[package]] name = "certifi" version = "2025.8.3" @@ -774,140 +637,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e4/d3/5268aeabf2ad82658c4e2ff3a060648d0f02f3926cb53247c0e4d0dab49e/griffelib-2.1.0-py3-none-any.whl", hash = "sha256:cc7b3d2d2865ad0b909fcc38086e3f554b5ea7acbaa7bbb7ecaa3f5dfb7d9f00", size = 142560, upload-time = "2026-06-19T12:05:38.742Z" }, ] -[[package]] -name = "grpcio" -version = "1.84.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/3f/4f/4435c0aae54657258d9cfcba78598f3d9e5fe4c82ff18d78558567b90faf/grpcio-1.84.0.tar.gz", hash = "sha256:19aaf172fc2edbefccce3f6e92c5150975dbe56c45744e9e87cf72ebdf85bfbe", size = 13493876, upload-time = "2026-09-14T06:59:33.291Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3a/4b/a0dc421d049b743093eae90caeb5dd92ced7226cd4919dc4de34c81455b6/grpcio-1.84.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:71fd60e6e426d293d0a2f685115ad0a0845117602cf13605a4be7524fb5f7bba", size = 6450049, upload-time = "2026-09-14T06:56:48.72Z" }, - { url = "https://files.pythonhosted.org/packages/f7/41/90292bf55af7aa09de0e3ec928d1b8c56d477f85244f7928d2231630b781/grpcio-1.84.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:8e1a45d174b6b8589f51dce1cea804aa6c1f72c9c80cba91ae2caabeb6d90540", size = 12344932, upload-time = "2026-09-14T06:56:51.685Z" }, - { url = "https://files.pythonhosted.org/packages/e9/68/b6c0248266a378b1bde08e4de7d69f3cc08ee6f5937a5dce7d3c3ba0fe1d/grpcio-1.84.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:efb29f8633bf6630dc89de4fe0353ac3d7e4b70ef7b6e29fb40f00e68c127fa5", size = 7030162, upload-time = "2026-09-14T06:56:54.412Z" }, - { url = "https://files.pythonhosted.org/packages/70/2b/0a2a2cbcf48847f83eb51fb982116d0965f2fe068e73f28b2d17facf30a4/grpcio-1.84.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:d0fdd25faece8a1f95e8a3a8006e29701b5cf8dadb4a8132e68f3134637004a5", size = 7781546, upload-time = "2026-09-14T06:56:56.571Z" }, - { url = "https://files.pythonhosted.org/packages/a6/7c/da97476f3c2e90e9f00bfb19def7cbb5f841b7661e3cd09c6a89beaa5b98/grpcio-1.84.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:393d8a78bff6731ecc5ad2151a821f8fbc1709b137ebb9c25a4ef399fbdcc914", size = 7186279, upload-time = "2026-09-14T06:56:58.817Z" }, - { url = "https://files.pythonhosted.org/packages/14/16/27fa3aed1ee6fdcbb978a1bd4bce255dc0122b90179535177a96543d14fc/grpcio-1.84.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:fc66cb50c93554b86db0b6625ab5c6e9051dbf8847c08d93c84918e02e413fb7", size = 7731191, upload-time = "2026-09-14T06:57:02.447Z" }, - { url = "https://files.pythonhosted.org/packages/4c/78/75644af37af85afb381376aef99cad92da8bc2d56ba3e5ae070a7cb59682/grpcio-1.84.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:455ed6083353b8e938f1d58c765eab2fbb165731e5b507be30fee344915a2a11", size = 8790443, upload-time = "2026-09-14T06:57:04.623Z" }, - { url = "https://files.pythonhosted.org/packages/95/4d/ce57fa986e93c06ef867f64e1ebe419e924fdc2395115607f0725f4855e4/grpcio-1.84.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d6a82c4fc6c85f2fb7572c86bdb86f84c97b6580e5f6599f711800bac48a5d8", size = 8138068, upload-time = "2026-09-14T06:57:07.348Z" }, - { url = "https://files.pythonhosted.org/packages/ba/a6/22a73111c4f75da9450bf0481fac805396ec9bf6f949a90bb2969de07cf4/grpcio-1.84.0-cp310-cp310-win32.whl", hash = "sha256:8e3f508d0e9e6236ba2f08d56e33355e434e785e813149a1b8477d3edf69779d", size = 4496545, upload-time = "2026-09-14T06:57:09.236Z" }, - { url = "https://files.pythonhosted.org/packages/31/ff/dc048bc3d8ebd8d4b7f6f6803c76142a9a5ca1e1e9fa34e79597f0f9ed77/grpcio-1.84.0-cp310-cp310-win_amd64.whl", hash = "sha256:ed2c1493c44d0932f1e55fdb5d1ead658c68288ec5d51b8c4928422d98633ef9", size = 5258144, upload-time = "2026-09-14T06:57:11.403Z" }, - { url = "https://files.pythonhosted.org/packages/2d/b9/46146728b3f4a5c7e34c17d0ab724d58b5456b116e76dc77d3ef4e79b135/grpcio-1.84.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:4aaeceeb7fa7d824c322d1ec3208c8495c88478a927295553235435fc49043ad", size = 6454572, upload-time = "2026-09-14T06:57:14.651Z" }, - { url = "https://files.pythonhosted.org/packages/e3/63/5d668b4102637410d700153fd12d6a798e3ff8308bd9dcbaeae93f191060/grpcio-1.84.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:06619ba1515e5ee69fb2a514e95dd8be05ce74cb3928d5b34f87f87c86fe3c27", size = 12359529, upload-time = "2026-09-14T06:57:17.202Z" }, - { url = "https://files.pythonhosted.org/packages/18/2a/52e29c02047a493f15a78c0502bde4d3fab7c19c7813944d367cd501811c/grpcio-1.84.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:158c1c11cfb61b4849c3caf4d52de6f5ecd376e14446feb4a90dc95a90d616f5", size = 7029927, upload-time = "2026-09-14T06:57:19.767Z" }, - { url = "https://files.pythonhosted.org/packages/0a/11/9962b313553647abb091943e0721e4a1662ecc63cdfe930abf00abcce47a/grpcio-1.84.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:a9383401d9f116f98cacd4eba6c505a6edb80ba65badfc8e8ed8ae64983bcc44", size = 7782268, upload-time = "2026-09-14T06:57:22.381Z" }, - { url = "https://files.pythonhosted.org/packages/e2/b7/14a9413cb7d4b2e782b4f79c81a918610caedf55138ab5916f5fdd4b002f/grpcio-1.84.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bd8ea8eb3817b226057cc1c0e7ec4b378dcda52043b972b6ff12b1152178967d", size = 7187959, upload-time = "2026-09-14T06:57:24.686Z" }, - { url = "https://files.pythonhosted.org/packages/ee/3b/6cc8e6aed8f23be40f52af341e5d4595ec3ec8d7572271a692b5c1212178/grpcio-1.84.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:756ea5c2da00fa65c930284892d2a9706828704ca3ba40b4c51c4834eb39fcfd", size = 7737554, upload-time = "2026-09-14T06:57:27.5Z" }, - { url = "https://files.pythonhosted.org/packages/3c/7e/6f61002a01802ca9675e1b3599c9b0f9f3cf168ded94ebacc02199309f88/grpcio-1.84.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:28d2609691da93051e998495108bbddd2a9f7a561253bae94828d81290f30c15", size = 8792681, upload-time = "2026-09-14T06:57:29.731Z" }, - { url = "https://files.pythonhosted.org/packages/eb/84/8bec1ae7e6732a9b435a394ddfdfffde46c2620ae0109823f7cce1a54455/grpcio-1.84.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:27b8b36200a9fbee6e120246f4a8a41657549107ef19fb2c819c4b2fd524f39a", size = 8145493, upload-time = "2026-09-14T06:57:32.672Z" }, - { url = "https://files.pythonhosted.org/packages/59/84/c8c7bd210d657288f18af06522f150f61e81ea14fd3c7c135beed697c5fd/grpcio-1.84.0-cp311-cp311-win32.whl", hash = "sha256:465eef3d17e59ad22a556fc0138f7c7c799df426734344daec42c797d49fda99", size = 4495596, upload-time = "2026-09-14T06:57:34.799Z" }, - { url = "https://files.pythonhosted.org/packages/da/1e/da99356b3b573af357d059753a47fba54f1ca1a9c0e4deccd0210cb7f4ba/grpcio-1.84.0-cp311-cp311-win_amd64.whl", hash = "sha256:f9a456bdbed52a01c9ab8423bdebab04a5363c78676edc55ab9b58bd13bdf9e1", size = 5259900, upload-time = "2026-09-14T06:57:37.067Z" }, - { url = "https://files.pythonhosted.org/packages/0a/c1/4c9a2e0e6b0aaf02781404cad2f79211f989f2c827cf672a4a48d1604d3e/grpcio-1.84.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:b5c6f20d657ae09ae4e30d9d3a21edd13f1219d58cc6f999b9d1bb63be9c1baa", size = 6415756, upload-time = "2026-09-14T06:57:39.345Z" }, - { url = "https://files.pythonhosted.org/packages/b1/57/131e7007bdee9acb77a8dbe8a16fa9fef75f88c1695242d8ee0993ac2d3d/grpcio-1.84.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:406583b4e8fb2282ebd392e12b963e601c1f82e07125a8c2cb5b144e7e024796", size = 12339195, upload-time = "2026-09-14T06:57:42.373Z" }, - { url = "https://files.pythonhosted.org/packages/db/d1/a7b7cda98fcab9b3d2916204a872d87371158a7a34e41768f524584fb64d/grpcio-1.84.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fbdbcd06986ede3ce584083b1dc2afe6808e8943e5cf50ad11183c03aceda25a", size = 6984468, upload-time = "2026-09-14T06:57:45.035Z" }, - { url = "https://files.pythonhosted.org/packages/19/81/c5be83e3ac9416f73c4c51fe1ea9c41a0c42fc3509e3505faa46f5046abe/grpcio-1.84.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:23e6e8e8a75cff88e0a793bfd3becea03a13e2763ae90c1ff573bc19ca5b429a", size = 7749432, upload-time = "2026-09-14T06:57:47.395Z" }, - { url = "https://files.pythonhosted.org/packages/a0/bf/258cd7c0a7ed92745dc93c31666d462d05b702807a689744bd49fb833bde/grpcio-1.84.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b44f0a0fc7bc6677d38cc80bca1a32814ce6c8f200fb8b3c1a61c9d77eaefbf3", size = 7156115, upload-time = "2026-09-14T06:57:49.657Z" }, - { url = "https://files.pythonhosted.org/packages/2b/4b/7f829418dbfcf91b875e55e2973f1059a95decb4f081313416317ef04ec1/grpcio-1.84.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:210e4c32f907045eb8158273e60c6ab69a3947697df6245dbda381f26c59485b", size = 7708010, upload-time = "2026-09-14T06:57:52.496Z" }, - { url = "https://files.pythonhosted.org/packages/34/f0/9932e2fec6a04205f8bf3f8f4d2020479dcdac88feb6f93822ed31bf0eba/grpcio-1.84.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a71d24f40b0cc6798feaa978c7411dc1135b7018e9fc0442db611c139bf58344", size = 8759980, upload-time = "2026-09-14T06:57:55.312Z" }, - { url = "https://files.pythonhosted.org/packages/2c/5c/b67407c6dbc480dfc0715f6eccdb1061e7c88d85f9a330a241d357a538c5/grpcio-1.84.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f6c972474ce691aca74e58d17625450cef153dc4760364cadeb167983ea6d589", size = 8124904, upload-time = "2026-09-14T06:57:58.569Z" }, - { url = "https://files.pythonhosted.org/packages/02/37/2bfdae2df8dfcfc0df619b628e0c7153ce703adae827243f44720322ccc1/grpcio-1.84.0-cp312-cp312-win32.whl", hash = "sha256:0d532ade4486dad9b302ffa4d4683d67561051c26d17c4023322845e9fa10140", size = 4478915, upload-time = "2026-09-14T06:58:00.714Z" }, - { url = "https://files.pythonhosted.org/packages/85/2c/309268b7b39f6deb2342f634841e105623a0b67982e8b10ec516782ff1c6/grpcio-1.84.0-cp312-cp312-win_amd64.whl", hash = "sha256:49717e857899f4136d7657bf5aded61ac479110a075438290923a4d86af7cd02", size = 5253534, upload-time = "2026-09-14T06:58:03.336Z" }, - { url = "https://files.pythonhosted.org/packages/5d/51/40f99701adb01d4e5316a2aaf13838da1a24d5c879cd8c95156d7c364454/grpcio-1.84.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:209414080da8c20af94df1395b635da52dd57b5edc9e917e1deca0dc1c4bb55e", size = 6427619, upload-time = "2026-09-14T06:58:06.025Z" }, - { url = "https://files.pythonhosted.org/packages/c5/4b/ed8e22a1237e6b2be6ef4f221d074a5b0e0dd8a0da8c944c04aea731f0eb/grpcio-1.84.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:e41c3993eee896c617dbd8a505085d28b6e84a0445ed9a1f40f95808473cf678", size = 12336549, upload-time = "2026-09-14T06:58:08.583Z" }, - { url = "https://files.pythonhosted.org/packages/d3/50/00165b05cd73f45996748ea67ce9e55d08936f2fea94a7fd8541cc2d0e54/grpcio-1.84.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fff5ef3fe1bba7d6147e5f19e01e5e122ac2c076486887ddcb8d42e663400fbe", size = 6989458, upload-time = "2026-09-14T06:58:11.884Z" }, - { url = "https://files.pythonhosted.org/packages/26/38/d0486230e684d916f97429a53041db88410e662a38f2a8d09e2d90375840/grpcio-1.84.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b8c62888c3e49debf37ad9773e3c02f77b0c1e811f8fb0962f2b6c3bbab5b97a", size = 7757778, upload-time = "2026-09-14T06:58:14.849Z" }, - { url = "https://files.pythonhosted.org/packages/da/56/548a643decb059ca244499c675ae2c13a15f523ba94592c2774bd80a13c1/grpcio-1.84.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:986e9751d416d7a6eaa2fecdac38da63153d63a4b340ba7d624889c490451500", size = 7159572, upload-time = "2026-09-14T06:58:17.87Z" }, - { url = "https://files.pythonhosted.org/packages/db/f5/42caac81a79ec680f1f7a8eaf7ca90d2f93936ce0c3a073141ba96757f77/grpcio-1.84.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5933a052946873d01a42119a05420d669bdca436aeba2d1851988ccb12b421c0", size = 7710547, upload-time = "2026-09-14T06:58:20.607Z" }, - { url = "https://files.pythonhosted.org/packages/57/a4/828ad990b2410fee0a55cc73aa1bf98eb5b911c54847374ef4f24b9e877b/grpcio-1.84.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e094dd21f077af8194923fc263cad872eaa1802bb0156fd7e5ae18e99cd86715", size = 8761519, upload-time = "2026-09-14T06:58:23.875Z" }, - { url = "https://files.pythonhosted.org/packages/d5/a5/1f91af098919eaf5d80d5a61126ad9fae074e5190c25a3014ce1d8d0d890/grpcio-1.84.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:08735e3d08d24ab3132cf87e2e5dea8746cabcc7d676c2b0b7362f195feef9d9", size = 8121424, upload-time = "2026-09-14T06:58:27.006Z" }, - { url = "https://files.pythonhosted.org/packages/8c/8f/77fd4a7a913b636785479922349c4cb98d94d05d15652e556b3ca0df6663/grpcio-1.84.0-cp313-cp313-win32.whl", hash = "sha256:70bb4ce8be0c5606bec259cbd7152374470396413b7863a658a08c849e6b29ff", size = 4477974, upload-time = "2026-09-14T06:58:29.528Z" }, - { url = "https://files.pythonhosted.org/packages/d0/9a/1fa59ddbfc8898e5518d1447e46f771f387f0ed6132ad531395338e51a5c/grpcio-1.84.0-cp313-cp313-win_amd64.whl", hash = "sha256:b61692f0069b3eee2fc8a3a1b7f6c044df9e03fede6ce69b3ca832e1c39f26c5", size = 5255326, upload-time = "2026-09-14T06:58:31.781Z" }, - { url = "https://files.pythonhosted.org/packages/26/6f/e25ca89ca5b0b7b95464c907a5c21a77c0ac8c4ee1dca164c4dd8f153ddb/grpcio-1.84.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:026d757df86c5b7a41de8200b9a2cda454aaa5004cb0c7e3374c66eb82f61499", size = 6428207, upload-time = "2026-09-14T06:58:34.401Z" }, - { url = "https://files.pythonhosted.org/packages/cd/b4/6b76b429f3f9b901cdbc306c81364d708bc957f847a05cbd1046cd2d05d8/grpcio-1.84.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:3de427b05f244ba2c2a9bdc67e7a6731c8340811524ecc4435466549f8af1d17", size = 12342420, upload-time = "2026-09-14T06:58:37.416Z" }, - { url = "https://files.pythonhosted.org/packages/af/64/ac86d638ba7f73bee0dccb608ba551d4f63adf75151f00d2c43e46d3979e/grpcio-1.84.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e90e3bdf7b5eac005fef631adae9cafde16f922def207b80a7c46b253c18ad20", size = 6998396, upload-time = "2026-09-14T06:58:40.535Z" }, - { url = "https://files.pythonhosted.org/packages/4a/65/fa12e9ec9d7ebf8cc3e81428fa9e1ca0d30d22d546ce2baa4c64bc917cbc/grpcio-1.84.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:e88d304f094f4937bc27ec6a435e218a084168f11ec630c8d5d39b431d08d81d", size = 7757538, upload-time = "2026-09-14T06:58:43.297Z" }, - { url = "https://files.pythonhosted.org/packages/21/d7/94240c7fae121ff1f116dcf04a3b7ee0216a06832c704310363f72638d4c/grpcio-1.84.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:57dc36a5ab0e676f5f6e171de2917fd0aef73f32a9aaf23956bfe19997a30bd1", size = 7161480, upload-time = "2026-09-14T06:58:45.939Z" }, - { url = "https://files.pythonhosted.org/packages/23/c9/7033e95d4b344969818b09185721c7608b47fc2498d97b5e4eec4995dbf3/grpcio-1.84.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5deda5b4bf62769eb98c119cca43d40e1231e34846b19db5cdea821d446a2253", size = 7720191, upload-time = "2026-09-14T06:58:48.308Z" }, - { url = "https://files.pythonhosted.org/packages/95/22/b45df2deba81d55069076859480bae7109c9eec02bce5515c799530cc2aa/grpcio-1.84.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9bab4cf571653a8afffb83ce21aa27b51dfe629b526b7b6adec35491fe1fc2ea", size = 8762792, upload-time = "2026-09-14T06:58:51.068Z" }, - { url = "https://files.pythonhosted.org/packages/de/c4/3e1c3d6155c16b8737cc31d5b477d6cf1fc7cdd10d58320cf0ec9b446f42/grpcio-1.84.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c5559b492007dc09b4de9b95dab05f0b5e53547aad230cf07e46c7dd017a3be5", size = 8123299, upload-time = "2026-09-14T06:58:54.332Z" }, - { url = "https://files.pythonhosted.org/packages/56/fe/f4864de5b815e5ba18858771f99381a398fac14117f89ef5291ed43d3c4e/grpcio-1.84.0-cp314-cp314-win32.whl", hash = "sha256:2c024da73b296f040b8360e60bd73a659b230093684a438da0e1260f34cc724e", size = 4562560, upload-time = "2026-09-14T06:58:56.894Z" }, - { url = "https://files.pythonhosted.org/packages/44/03/640811d4d8c84f5e603995c5a9bab725223aa472cad9ca4286c3bbf1c3e3/grpcio-1.84.0-cp314-cp314-win_amd64.whl", hash = "sha256:800b7e00d92553313c0463c200087930aa78678ec1d528193aeb50906f55989b", size = 5394092, upload-time = "2026-09-14T06:58:59.61Z" }, - { url = "https://files.pythonhosted.org/packages/4a/1a/9e3d2c9f005f680f03308fa894b1db91d4ab3f0fe65ff630c69561e91e95/grpcio-1.84.0-cp315-cp315-linux_armv7l.whl", hash = "sha256:47ecf0d9b81d981f07b61bd89eced9d2582f5eaacc3aaa36ad27f81aef70a27f", size = 6428252, upload-time = "2026-09-14T06:59:02.597Z" }, - { url = "https://files.pythonhosted.org/packages/77/34/0bc9f52ebf091311651eeab3a452fb557985604a3088cb5406f4d6df85d3/grpcio-1.84.0-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:61386101ecaa096b694d0dd278caf99a56aeec78440cc17e918eef0b50f2d567", size = 12359488, upload-time = "2026-09-14T06:59:05.646Z" }, - { url = "https://files.pythonhosted.org/packages/93/0e/c31052712f241cb6ecae9c226fabd519b7f8c64a7a40bac27e9ca0405b78/grpcio-1.84.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f6d178ba6dc8e82976c184b65fddde172d054c17237993a3e083efe4f134d55b", size = 7019339, upload-time = "2026-09-14T06:59:08.76Z" }, - { url = "https://files.pythonhosted.org/packages/55/b9/b9b33ea4f1eb4cad28833cade604febf357385b5ebb0c9c7562d020e167a/grpcio-1.84.0-cp315-cp315-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:15bb76489e337fc492685c9758e2fd4d4ab516b901ad830dc5a91987decf00be", size = 7107974, upload-time = "2026-09-14T06:59:11.568Z" }, - { url = "https://files.pythonhosted.org/packages/0e/9e/799d4c45db91bbdcd8c54b3982932dbcf3d059f7ce67dca3e8540faa1ece/grpcio-1.84.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:82da34ae4f639c73ac46e521e00c0a49bf86f717b9fb1f405f133e98731e38dc", size = 7200036, upload-time = "2026-09-14T06:59:14.401Z" }, - { url = "https://files.pythonhosted.org/packages/45/dc/dcfdd13ada41aff9098f0c2c6f260eb7debbc88b84b7e5fcbd085165427d/grpcio-1.84.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:9b73836ba0e16fcbb57c31cf6cbc2907c8d8c790b83679df454b74bd15e0be04", size = 7742281, upload-time = "2026-09-14T06:59:17.348Z" }, - { url = "https://files.pythonhosted.org/packages/55/31/75eab2ec77b80804bc5e21cec99b57598e726fca6484cd3e8920a97639d5/grpcio-1.84.0-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:42959bd50dd660ffc3f2a9bec15a6da4f9aaa0dda555d59ff2d2e80b908456a8", size = 8113629, upload-time = "2026-09-14T06:59:20.584Z" }, - { url = "https://files.pythonhosted.org/packages/34/f0/fdcf6bdc1df9ca11679a1187bef8e6b81df31a2baae69497e17344f05ea3/grpcio-1.84.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:659728f20fc7a0933ed7b1945435e31014b97ab8a5a7edcbaa70da4794aeb191", size = 8152972, upload-time = "2026-09-14T06:59:24.523Z" }, - { url = "https://files.pythonhosted.org/packages/5c/cf/6720e720bfa80fcb1ace873f66724eb3c8b03bba2fa078a30c12cab3212e/grpcio-1.84.0-cp315-cp315-win32.whl", hash = "sha256:edb6f87fc60ff438557291501b3e16c7a77c3b01a52d782cf276dccc7c5dd89c", size = 4561981, upload-time = "2026-09-14T06:59:27.275Z" }, - { url = "https://files.pythonhosted.org/packages/7f/b9/69d8a709df225bc2e06e028e9465166b174c24b3da07cc72d9a5ddc63194/grpcio-1.84.0-cp315-cp315-win_amd64.whl", hash = "sha256:4119efa6519871719ad81f33bc95ab87857dcb1c5801f30a6e592f2c41164169", size = 5394757, upload-time = "2026-09-14T06:59:30.118Z" }, -] - -[[package]] -name = "grpcio-tools" -version = "1.81.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "grpcio" }, - { name = "protobuf" }, - { name = "setuptools" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/83/b3/1c5951352d6777fd7f99a0ccee04617fdfd8a5dbf2918a1f58c8b2b280b8/grpcio_tools-1.81.1.tar.gz", hash = "sha256:a22a3870180927fdd84e2b27d079ef5b7f5f8c6110181b6736afc17a463481f1", size = 6236155, upload-time = "2026-06-11T12:51:21.235Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/e1/1fcf884902ae7255d8da224cfa638ea88a46d50f62a33d06d35c8960b029/grpcio_tools-1.81.1-cp310-cp310-linux_armv7l.whl", hash = "sha256:9b6ba8a72cfda576508701a7c0bbeebe6f6f9843320d4f12e74efd19ddccd965", size = 2586261, upload-time = "2026-06-11T12:49:21.447Z" }, - { url = "https://files.pythonhosted.org/packages/a2/d7/1815110b2d40ec99dbb0a7e6d7eafd591cd1f1e9bf9d3858cd9cf3ffacbd/grpcio_tools-1.81.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:ac47a9ea1224df8b653072614e6f0207e9fbfe63fdabaa5918a60ca5fc931b88", size = 5817509, upload-time = "2026-06-11T12:49:25.958Z" }, - { url = "https://files.pythonhosted.org/packages/23/e8/af99579842b5a555312fa782f32ce0f99bd35b2b7a1243294b2755468857/grpcio_tools-1.81.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eac4bb645ceff0c147cc720a40ae68f97427eaafb4968e866dd8fcc20d3d4831", size = 2634112, upload-time = "2026-06-11T12:49:27.937Z" }, - { url = "https://files.pythonhosted.org/packages/b5/cd/235ad56ac728c49c17e9218c4daccd5831e6ec7af94236bec0cc66c71c68/grpcio_tools-1.81.1-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:cc410b621dd85193766c12dca2e238696199a27a65d2b31b6f0a4c6c0043ff26", size = 2957950, upload-time = "2026-06-11T12:49:29.619Z" }, - { url = "https://files.pythonhosted.org/packages/77/3e/9103e8b4610597bf89db49eb112091c91bf5d63ddef2a951e11a4be05f2b/grpcio_tools-1.81.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b62d254c214faa3773eac709376ae25cf7abff1a76ba5fc4dbcd7b14fc4e4ae6", size = 2697765, upload-time = "2026-06-11T12:49:31.702Z" }, - { url = "https://files.pythonhosted.org/packages/3c/86/beb2a43fbb93570a2305696083f6736566301d957869f463308ec6839f95/grpcio_tools-1.81.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:bd0b68dc76b10b3384b9b6e9f59202b83dcaafd8098eb644759a69316686acf8", size = 3147588, upload-time = "2026-06-11T12:49:33.748Z" }, - { url = "https://files.pythonhosted.org/packages/af/4d/b0182d9948631cd837a372b6625cf59d6e335d4aab0f425d4b7306619074/grpcio_tools-1.81.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:a28d231455ab6e3558299f7d831a73c8be8ee6b7ec614ecf39eb50c0ed15767f", size = 3708798, upload-time = "2026-06-11T12:49:35.979Z" }, - { url = "https://files.pythonhosted.org/packages/23/9b/f452a189d399051d85cf82fe2f27a070efaa52512a2c5e3ae6ef1ae99a1f/grpcio_tools-1.81.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:82740248eb6f3b6a38988cb5e64adb7303af9ea5cb4197c8ed08c1fabc767440", size = 3366969, upload-time = "2026-06-11T12:49:37.911Z" }, - { url = "https://files.pythonhosted.org/packages/9b/48/0075cb4f6ae7db280f461de2dbba700b22ae62e351ae13e6e461cd6804de/grpcio_tools-1.81.1-cp310-cp310-win32.whl", hash = "sha256:801d9d8ab5cddf8f8e064225292f0713427011252a07828a6b54e2ed64d534de", size = 1008713, upload-time = "2026-06-11T12:49:39.791Z" }, - { url = "https://files.pythonhosted.org/packages/17/bd/7692bc698259e5645b68720e77e7b176d376f6ae0c9db8b5b750a02f1958/grpcio_tools-1.81.1-cp310-cp310-win_amd64.whl", hash = "sha256:3c8611d6e4e859ac5373422ef27c4b7540cf98c9991c9abc6722613ef72b13aa", size = 1174752, upload-time = "2026-06-11T12:49:41.43Z" }, - { url = "https://files.pythonhosted.org/packages/18/76/14ff87090199a36f914388299a1148d0734a20cea1b0ca8480bae1f373f1/grpcio_tools-1.81.1-cp311-cp311-linux_armv7l.whl", hash = "sha256:8161f398f957a376cae7385ea7c8684f439d460ef702b528912da3bcb31fc515", size = 2586251, upload-time = "2026-06-11T12:49:43.514Z" }, - { url = "https://files.pythonhosted.org/packages/87/a8/d5aa99de9d8b2dd2a8192c1779796eda8b0d0f1dd915422e0a8a61b80391/grpcio_tools-1.81.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:53ef76cc3b0493ff734a5e8c39d5b519e1822236fcccdfe7677c5e1efd767761", size = 5818063, upload-time = "2026-06-11T12:49:45.975Z" }, - { url = "https://files.pythonhosted.org/packages/ba/cb/2e9a6dbc6a514dd3cd264fb3bf9217937453a4d45dbc3ca6ca4ee34ba1a7/grpcio_tools-1.81.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:690e6dcaa8b8a7886ce206ba344e2127211597e1a1ddab73df9f3d80c8f6707e", size = 2634061, upload-time = "2026-06-11T12:49:48.13Z" }, - { url = "https://files.pythonhosted.org/packages/c8/2b/2ccd1a929e6c8ad84a0aa8d66ad9f615b4a8e79d9927373d86aa36b4ba2e/grpcio_tools-1.81.1-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:ad7a997c07bd345e84842e60561e7e2cc090ce6c4e1d2f0407e31b85b40fc49a", size = 2958029, upload-time = "2026-06-11T12:49:50.466Z" }, - { url = "https://files.pythonhosted.org/packages/e7/67/2da8cd312edc348f44f26f82096b25cdb7d2905cd786acc6bf777b169502/grpcio_tools-1.81.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b6bd163ece4535726e5292b845ed80ae9b2cae73ba091c7d6c66033c430e3857", size = 2698031, upload-time = "2026-06-11T12:49:52.292Z" }, - { url = "https://files.pythonhosted.org/packages/d9/ba/ad1680fbdf9317c4f1e54c37c96d1f422370df66ac9adbd175c7cb3531d7/grpcio_tools-1.81.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2baa7e735f35b2a648144c03348a126097b13e101d3c242d5edb6ac91437ccbe", size = 3147541, upload-time = "2026-06-11T12:49:54.43Z" }, - { url = "https://files.pythonhosted.org/packages/57/c1/57cd08eef293d713cb8935295e4f08d8f0013480b2ba3aad1af0271eb7ba/grpcio_tools-1.81.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1d602b410b2b2addc434cace9ce4fe2035974a3078228f98ffa049a5c90acc2f", size = 3708524, upload-time = "2026-06-11T12:49:56.544Z" }, - { url = "https://files.pythonhosted.org/packages/52/31/01ea8ca9c82fe2c79b5b594c3ae427d56699bc106b2d91caca129add8b10/grpcio_tools-1.81.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f8cb64f87c45ccca8234fa47e6b21f09e43801ff11b556deecb461b3b3e9f292", size = 3367022, upload-time = "2026-06-11T12:49:59.608Z" }, - { url = "https://files.pythonhosted.org/packages/7d/35/8140cd175602df3d17215cfb28a7ea55b7a67e2b872be76e1ee4af5c4df9/grpcio_tools-1.81.1-cp311-cp311-win32.whl", hash = "sha256:87b25ca0e27373a4a32a629a4ba976f5764b9887dd50d6fe017d38009a0363e8", size = 1008980, upload-time = "2026-06-11T12:50:01.422Z" }, - { url = "https://files.pythonhosted.org/packages/be/86/1bd29ab3c52457702b96536f1f208ab27695322d855f95c9666dfb713019/grpcio_tools-1.81.1-cp311-cp311-win_amd64.whl", hash = "sha256:204de03b539a4b08772c6553b92bcc112cbc965e0ac22f909f6d133b8ac33a8c", size = 1174840, upload-time = "2026-06-11T12:50:03.408Z" }, - { url = "https://files.pythonhosted.org/packages/3b/8a/824a9ca20bcdce8a568bb8c9f98bfeb7fad62129235e6d2ae7576fd1250a/grpcio_tools-1.81.1-cp312-cp312-linux_armv7l.whl", hash = "sha256:353b1fafcc739c31ed42271052709595b340d34f27c459beeb78a32938305bb5", size = 2585927, upload-time = "2026-06-11T12:50:05.671Z" }, - { url = "https://files.pythonhosted.org/packages/2f/35/e5f9f671378b1b89a896150d3e4fa2c6ec61a5e1e9e5107ce4c140ccc931/grpcio_tools-1.81.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:768f584c2423cbeb6cb6867817a39365b987ff16b8259a3adbc6546b9e303a4e", size = 5815665, upload-time = "2026-06-11T12:50:08.178Z" }, - { url = "https://files.pythonhosted.org/packages/c6/02/631b628e4072e988c669bd8f1b2406ef3c9a4cfcb2625bbf2a308a07b71d/grpcio_tools-1.81.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1680b35a84f4694401819ac4acac42dda6dbc7bb8fc74112fd1a60425a07adf4", size = 2635518, upload-time = "2026-06-11T12:50:10.391Z" }, - { url = "https://files.pythonhosted.org/packages/de/7c/2e3537e3ea3d1c0ddd6766cf6a7c62b487d89fb005713df2781d5f21483a/grpcio_tools-1.81.1-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:f64e665c8ec639278ecf009beb92cbdcc5994f617c1af3d58036e1f70b1423ec", size = 2958252, upload-time = "2026-06-11T12:50:12.677Z" }, - { url = "https://files.pythonhosted.org/packages/35/68/14013cb2942bdac354746b643b4c37dd91906da8dce00f41c616e88bf33d/grpcio_tools-1.81.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ba8f1ae82ad199f43448995715445cc623fb20d3882382e4be61f0da8ccb3f0e", size = 2698439, upload-time = "2026-06-11T12:50:15.017Z" }, - { url = "https://files.pythonhosted.org/packages/bd/45/000c14c0338a7ad36054b9f17ea41842deb7841c05c067dd36cc831bc0f4/grpcio_tools-1.81.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b7b6d1e986d5923751bfe2b5cca9c4cb3d5653446e4fa4aacd438033e2dc360a", size = 3152160, upload-time = "2026-06-11T12:50:17.3Z" }, - { url = "https://files.pythonhosted.org/packages/41/97/881930ca3967d2c8a95649bea8ebc991a7cf2331bc96679fd3600450dccc/grpcio_tools-1.81.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:7f208c207aca639dcb34648d3826c38d7cf3485118fb2065117e9fc4827406b3", size = 3710468, upload-time = "2026-06-11T12:50:19.479Z" }, - { url = "https://files.pythonhosted.org/packages/c7/b5/67baeba7366162652cdc1dbd962289accde07241bc8f42f6f02b305efcc6/grpcio_tools-1.81.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:724ecb69af63d2f6d4ccea3e6fa0ca110ed9c5824d48c2f887c631bbb03c1c3c", size = 3370797, upload-time = "2026-06-11T12:50:21.501Z" }, - { url = "https://files.pythonhosted.org/packages/f2/5d/34f2dce2125ccb107e32b57f5a9c1257edcc0793b0d2fef1e8b13a6bac3c/grpcio_tools-1.81.1-cp312-cp312-win32.whl", hash = "sha256:895a6782cec86beac71ccebb4b9848259c6f04a3028b8e42fa8d40cfe5146593", size = 1008453, upload-time = "2026-06-11T12:50:23.358Z" }, - { url = "https://files.pythonhosted.org/packages/8a/be/09da8256ec8d2a5ce8a1acc51cbbc4ca52a462d78ed3412778440a56502e/grpcio_tools-1.81.1-cp312-cp312-win_amd64.whl", hash = "sha256:0265fd1386b7458302f79542558345880d484f8fa92ae196c0c0268242c5f23a", size = 1174857, upload-time = "2026-06-11T12:50:25.685Z" }, - { url = "https://files.pythonhosted.org/packages/76/90/5faa8b26e03495e5117f93bef8293cbada4af136362745dad7d1813ef0b0/grpcio_tools-1.81.1-cp313-cp313-linux_armv7l.whl", hash = "sha256:3d604b4fd114b79ebb9f865bf3e04fd3ae93c704e1fad96f7fd03b0865c263b7", size = 2586071, upload-time = "2026-06-11T12:50:28.4Z" }, - { url = "https://files.pythonhosted.org/packages/e8/9a/85dc589fa6ae2439451eaa81a1578de31e29c676980d38bef7549b8a1f45/grpcio_tools-1.81.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:3389e705460efa3f3758141ba5520e6743b131c9576197c944fb9cbe49048126", size = 5813299, upload-time = "2026-06-11T12:50:31.295Z" }, - { url = "https://files.pythonhosted.org/packages/77/fd/c53994e58a837e6eefe48f53eb3492afc04f2b8af255df4adb37d14378f8/grpcio_tools-1.81.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8a17d8ceeb6a855fadf39f5171c80a382d97c4db98d5943eca553497fdebf84b", size = 2634668, upload-time = "2026-06-11T12:50:33.938Z" }, - { url = "https://files.pythonhosted.org/packages/34/32/de988e86688686a2117e7ce6ce9eff4f638c929bb55b0afe60d6fbd2e45c/grpcio_tools-1.81.1-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:43baf71dc60fd653062da2e95e95c73b35dd130be8f9fa3d544c3af3f808a290", size = 2957930, upload-time = "2026-06-11T12:50:36.726Z" }, - { url = "https://files.pythonhosted.org/packages/72/97/3f18a0ea32b5f809d21961dbd0bc382b589a4c3d501e3d67c345d5456ed3/grpcio_tools-1.81.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:136e90906af0df51ad929713244ba812d0dbb1844b4f467d5d86bdb054698f90", size = 2697760, upload-time = "2026-06-11T12:50:39.108Z" }, - { url = "https://files.pythonhosted.org/packages/49/c0/dbf5cbc877290ff7504a59959a8af4fdcfdaa1e84237948405ccf1aa82a6/grpcio_tools-1.81.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd6c3bf3ea6a61eb58c54368d72ada591f2a270f3a31a32e8536e773337e76d9", size = 3151456, upload-time = "2026-06-11T12:50:41.983Z" }, - { url = "https://files.pythonhosted.org/packages/de/ea/16fe2dc83140a59e5c0a0b9dc2693dd36bfaa6bd835724b4ec66a68eab7b/grpcio_tools-1.81.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c306c307f8f74cddc4056fdbb6f1da55de087a21120efbd02bd915daa5a52fd", size = 3710469, upload-time = "2026-06-11T12:50:44.596Z" }, - { url = "https://files.pythonhosted.org/packages/22/7d/df987d7d81e7ad2f7516d9e9d56ff29c54dbc6d8587e425688dca9a28e49/grpcio_tools-1.81.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bdbdc927be2e0ea13c32564a72ee31d712a716fb6f8c0d53d37a77d8277c272c", size = 3370488, upload-time = "2026-06-11T12:50:47.199Z" }, - { url = "https://files.pythonhosted.org/packages/ba/c5/5a63444d694ea47bf670138208f71830cc1759c402c8818092b28ab2dc5f/grpcio_tools-1.81.1-cp313-cp313-win32.whl", hash = "sha256:9d383724bcd67244b6def9e9164c640ee9380c0b7534ee7545a6fb0022a59afe", size = 1008229, upload-time = "2026-06-11T12:50:49.527Z" }, - { url = "https://files.pythonhosted.org/packages/00/75/3945e26d5c94ae6ed9be5caef73d4d66c47dc8cfdd7b4995efaf942754e0/grpcio_tools-1.81.1-cp313-cp313-win_amd64.whl", hash = "sha256:f3eb15849979ca7bb864ce81a74d68b0f225a7f111ed3fe212bfc08cf9812b10", size = 1174523, upload-time = "2026-06-11T12:50:51.755Z" }, - { url = "https://files.pythonhosted.org/packages/0d/08/e581ad42ae517a61172285047e4d710e2ac75f2f1915f7c91f284254e6d5/grpcio_tools-1.81.1-cp314-cp314-linux_armv7l.whl", hash = "sha256:7d168ea26390717d0462c0d0408331dc98a60fc7f7e6118afac9b73f5a66d87c", size = 2585944, upload-time = "2026-06-11T12:50:54.528Z" }, - { url = "https://files.pythonhosted.org/packages/78/c8/200d90ebad685af7eea5ff7e0360c504dd01ec053fe0f1f9c4abe3ea2d5a/grpcio_tools-1.81.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:43c528655b226375013036692d8db4cd59060c1f41dd62c77f4d17b69f6ce828", size = 5813492, upload-time = "2026-06-11T12:50:57.291Z" }, - { url = "https://files.pythonhosted.org/packages/2d/c0/60da2a1af37aa8eb47308cec24d9f7709a8976fdec3a53fd35b56b358326/grpcio_tools-1.81.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a9c6fcc68c9d5a208967bfe4fd3224d3c3be9a950c3e827e8f4b17e15c2dc555", size = 2634991, upload-time = "2026-06-11T12:50:59.767Z" }, - { url = "https://files.pythonhosted.org/packages/0c/7f/dede28b579ae9bf9079ba1aa913e8088d1dc0cdbe21c85caa22f0790cad2/grpcio_tools-1.81.1-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:a987c85dcbe1b32066d7acd46266d1a428aecbd629331bf5b853e74c835bf876", size = 2957913, upload-time = "2026-06-11T12:51:02.31Z" }, - { url = "https://files.pythonhosted.org/packages/4c/38/4de2118adb58ec7ffba65ec623b5836db769665c192517cbf187db3f6145/grpcio_tools-1.81.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a882382507bb5ec6d7edc9648053dfd3bc8f9285cde56a6fa9b9a83b4bd07f1c", size = 2697709, upload-time = "2026-06-11T12:51:05.016Z" }, - { url = "https://files.pythonhosted.org/packages/6b/e1/762ced51059e4f694fd337ecae491581d42a4e61dcb0415d8c5c60e6ddcb/grpcio_tools-1.81.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7746e508d4239a02f7e93638be5bc0ebb0120ddb796f7506aaae9d47a4599d97", size = 3151884, upload-time = "2026-06-11T12:51:07.593Z" }, - { url = "https://files.pythonhosted.org/packages/19/d8/9823090dc801e7229944874e7429c3b98e741ac778d8dc373f60240e1c43/grpcio_tools-1.81.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:fc3d2a41a7a4467fa03b391394fffada9291fe8feebc8679b526f6bc36942b25", size = 3710404, upload-time = "2026-06-11T12:51:10.172Z" }, - { url = "https://files.pythonhosted.org/packages/64/4e/4eae98d02148cb6f9f452f09942afba407afa6851e6c1fddc5ae9ec0b4ed/grpcio_tools-1.81.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:21bb3ba90e6d8df1ff663d4ee39a4e5b25a64e8ed4902476ca9ded0954d3917a", size = 3370525, upload-time = "2026-06-11T12:51:12.678Z" }, - { url = "https://files.pythonhosted.org/packages/e0/3e/2206e597a128da6a03a6106d2eaf2c3e72c7d80843d4be933e3a3d10d02a/grpcio_tools-1.81.1-cp314-cp314-win32.whl", hash = "sha256:3dca56016d90a710c4d9861bae793dc089c1430a90c79ce672e948ddb65fa539", size = 1030582, upload-time = "2026-06-11T12:51:14.906Z" }, - { url = "https://files.pythonhosted.org/packages/cf/f2/bbeef86c687225b7bbc7c0acdfbd25c8bcaa3f5b1c941db053e5c3d9e859/grpcio_tools-1.81.1-cp314-cp314-win_amd64.whl", hash = "sha256:cb08172b7b629e75cb33866928d319a3196540a725eaab628ba721007140f1af", size = 1207490, upload-time = "2026-06-11T12:51:17.598Z" }, -] - [[package]] name = "h11" version = "0.16.0" @@ -1848,52 +1577,6 @@ dependencies = [ [package.metadata] requires-dist = [{ name = "mcp" }] -[[package]] -name = "mcp-transport-examples" -version = "0.1.0" -source = { editable = "examples/transports" } -dependencies = [ - { name = "aio-pika", version = "9.6.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "aio-pika", version = "10.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "aiomqtt" }, - { name = "grpcio" }, - { name = "mcp" }, - { name = "protobuf" }, -] - -[package.dev-dependencies] -dev = [ - { name = "cassetter", extra = ["grpc"] }, - { name = "coverage", extra = ["toml"] }, - { name = "cryptography" }, - { name = "grpcio-tools" }, - { name = "pyright" }, - { name = "pytest" }, - { name = "ruff" }, - { name = "types-protobuf" }, -] - -[package.metadata] -requires-dist = [ - { name = "aio-pika", specifier = ">=9.5" }, - { name = "aiomqtt", specifier = ">=2.4" }, - { name = "grpcio", specifier = ">=1.71" }, - { name = "mcp" }, - { name = "protobuf", specifier = ">=6.33.5" }, -] - -[package.metadata.requires-dev] -dev = [ - { name = "cassetter", extras = ["grpc"], specifier = ">=0.11.0" }, - { name = "coverage", extras = ["toml"], specifier = ">=7.10.7" }, - { name = "cryptography", specifier = ">=50.0.0" }, - { name = "grpcio-tools", specifier = "==1.81.1" }, - { name = "pyright", specifier = ">=1.1.400" }, - { name = "pytest", specifier = ">=8.4.0" }, - { name = "ruff", specifier = ">=0.8.5" }, - { name = "types-protobuf", specifier = ">=7.35.1.20260906" }, -] - [[package]] name = "mcp-types" source = { editable = "src/mcp-types" } @@ -2019,187 +1702,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e8/3d/1087453384dbde46a8c7f9356eead2c58be8a7bf156bca40243377c85715/more_itertools-11.1.0-py3-none-any.whl", hash = "sha256:4b65538ae22f6fed0ce4874efd317463a7489796a0939fa66824dd542125a192", size = 72226, upload-time = "2026-05-22T14:14:28.824Z" }, ] -[[package]] -name = "multidict" -version = "6.8.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/14/95/989c1b5ca17b72128661530cd6e351a0a83cda9a4d6c036e9ed976c18931/multidict-6.8.0.tar.gz", hash = "sha256:5cd4637ce76312ba1e05eb9c5193fec231f64fee0944e135fa1e951242355b37", size = 122412, upload-time = "2026-09-09T13:57:57.967Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2e/ec/714516a7e0f0e05bd5f67402bdeac775e0a50b883eafca3cf21adcacc228/multidict-6.8.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c0fe437a6d2f36aac2b49517057776575b5bf359df314cca20d230a6e139c089", size = 85595, upload-time = "2026-09-09T13:52:49.534Z" }, - { url = "https://files.pythonhosted.org/packages/c5/af/13c6c983bb2a59a567fda78ecc42bc3328889179480cc14389213f6837a8/multidict-6.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f3a0a31189acf6703307397c6139ddabd734c20c5ef92649fc93e473df6615a3", size = 51300, upload-time = "2026-09-09T13:52:51.225Z" }, - { url = "https://files.pythonhosted.org/packages/6d/59/38746cd2837b3656247d841c28bcac821be312319c89ea126ec335077ff4/multidict-6.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c9c98d2f0126ba84cb45601eed97ff67ff767e19ae6eb3c31b02827b54d700e5", size = 50492, upload-time = "2026-09-09T13:52:52.527Z" }, - { url = "https://files.pythonhosted.org/packages/aa/fe/9b1b44d060692fbeef47e7f9d72f9cb9e9c2c2fb8381fe543419449f5b7f/multidict-6.8.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7bc7003991ebd368a20d05228137a37b3d3066751f3ea1e4f7b8efe8e752f2f5", size = 259705, upload-time = "2026-09-09T13:52:54.082Z" }, - { url = "https://files.pythonhosted.org/packages/01/6a/dfb3e47ab0efcab2ddae494c86d9ac1fd47c86b3ebf687b1d6bf72221178/multidict-6.8.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c2c5fd0fd39574ccd58e1a52565b341aff522c5c836f1b3eb7605c371e61f52c", size = 258089, upload-time = "2026-09-09T13:52:55.647Z" }, - { url = "https://files.pythonhosted.org/packages/91/33/abc20faf78cd7060d4f904f0e669cc521537b1a2a0f6f8ad84cf5006447f/multidict-6.8.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b1cd4d66ce894a45482e1ac2837c31d0bd447df35065e542b60055aa2d00404b", size = 235981, upload-time = "2026-09-09T13:52:57.291Z" }, - { url = "https://files.pythonhosted.org/packages/bc/79/fba4622994740f487c927d7331876b1cb7253515bda66d8fb7bc0a382879/multidict-6.8.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7a2573d0fd34f361a4a14e54d8cda3a91ac4e55fbf0d719698024f3b09c5b147", size = 269134, upload-time = "2026-09-09T13:52:58.726Z" }, - { url = "https://files.pythonhosted.org/packages/de/6b/518eb2f391c9e579afb08d67fa280c4037d3b61fe57bc1071e6b84306cd4/multidict-6.8.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edda19aff836ec515caafc09ea53d2ab144a041f09ee9a7cefcbd3ae4e976256", size = 271441, upload-time = "2026-09-09T13:53:00.108Z" }, - { url = "https://files.pythonhosted.org/packages/53/d2/6db1ce7dc516d4b9afbe679b0e7190b26a517b25ccd11122c8c27ed098a3/multidict-6.8.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e418ec99574ca24365ca96546af285c2b021a1a072478a79f0e3cc3b08837154", size = 259314, upload-time = "2026-09-09T13:53:01.574Z" }, - { url = "https://files.pythonhosted.org/packages/7d/b7/548ec8cb0e3be3c03519420de5bc3094418254d8efa599da7e39a567c585/multidict-6.8.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:11d71490bf4bbff1141b14b93af419ad68c56b60bea9277fcb3f94dcca4796eb", size = 243703, upload-time = "2026-09-09T13:53:03.067Z" }, - { url = "https://files.pythonhosted.org/packages/f2/20/2af67fcc8aa6ee8727ae9a29fbebc81bcce5f9d78f8a6c365638824ffb09/multidict-6.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e8e1e895e23818d343e4ae7dd95a0a556fdeaf8b471acf1c0a39b93c6f54d478", size = 254376, upload-time = "2026-09-09T13:53:04.514Z" }, - { url = "https://files.pythonhosted.org/packages/0c/2b/b41aa60b0a1021304e95444aa40e3ff7a2a31028a21dd1d93c628de5b87c/multidict-6.8.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:15db8e6cab5f4cc9241bc56e69fdf3452cf49c10ee3c7977c742e68a275b3786", size = 249211, upload-time = "2026-09-09T13:53:06.056Z" }, - { url = "https://files.pythonhosted.org/packages/5d/e6/ad76aa06f5aace4ab82e70367525ef9b0606333963d9e436273c574c10ad/multidict-6.8.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:8dc2d9c3a924ed14166e63650b2cf9f59e7821743bdd50b23802bd97ca09bde5", size = 260855, upload-time = "2026-09-09T13:53:07.528Z" }, - { url = "https://files.pythonhosted.org/packages/19/45/248ebbd3276a6c066e631f7a49c7c2c0e0774600a752144e64f0ae9af82c/multidict-6.8.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:f2fa3d3b1c933d4bcb8fd2018700d5e7235c52f2ab8c88d22286965c5c0f00f8", size = 266149, upload-time = "2026-09-09T13:53:09.097Z" }, - { url = "https://files.pythonhosted.org/packages/76/72/3d87c20cd944a1eb9bcb21928d3e39e758aea8b0e4df3defc82d081f6c7f/multidict-6.8.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d6dc7804c50fabd28644d4d18a4b20aad3681b3e64f3acd3182b330ca73f7a32", size = 239270, upload-time = "2026-09-09T13:53:10.502Z" }, - { url = "https://files.pythonhosted.org/packages/2b/ff/8a69b1ecbe25cfdbf5200167357dc01ecf3abb974cb473fbaac5d3724e79/multidict-6.8.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:0eca15d627e942ce186a935061f1568cc46c02e97c419c8da802df2be9f917d8", size = 262229, upload-time = "2026-09-09T13:53:11.944Z" }, - { url = "https://files.pythonhosted.org/packages/fd/e2/a40b690a319c3a17e1ef3cad127a7993d69cec06c892c9000cf67329cf75/multidict-6.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d7e5ba0a0153e35fbce9c51df530c8b4cb0c3012b46a04ff9a048441a269c2ed", size = 255270, upload-time = "2026-09-09T13:53:13.412Z" }, - { url = "https://files.pythonhosted.org/packages/99/bd/0e72f5981012a66ebf8b97153de8ff299b947af12d62beb832626f7a9b0d/multidict-6.8.0-cp310-cp310-win32.whl", hash = "sha256:b7e62b8fc7bd6cad007b9f2e0ad9c8d4854c06350d5f51e1a439dd18b510ecac", size = 46896, upload-time = "2026-09-09T13:53:14.799Z" }, - { url = "https://files.pythonhosted.org/packages/0d/43/7f93a35715d1ce1d96a7aedb7bffa870206390c0e2ba7cffd9b8533739a1/multidict-6.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:dc911ae6152e455b16a2a1a626aa6cd612fa01efb9d0a4ab3f5cf328b911483d", size = 51492, upload-time = "2026-09-09T13:53:16.067Z" }, - { url = "https://files.pythonhosted.org/packages/b5/d7/518dbe7eb714f413a093ef32c99010411e228ff57fe3e1bfb2df80abccf2/multidict-6.8.0-cp310-cp310-win_arm64.whl", hash = "sha256:6300d5176647145ba1e22991c924fb29743e54b4d7b8bc85a0d3ec0e55e189cb", size = 48049, upload-time = "2026-09-09T13:53:17.3Z" }, - { url = "https://files.pythonhosted.org/packages/d4/37/90216392620b6ef8704eb0bc055141745de396121067e98f1f72bdac33c3/multidict-6.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1b8986d4313dcee7c932837d16a535f1840b827bac1ea7c5c4c80751d0423794", size = 85033, upload-time = "2026-09-09T13:53:18.786Z" }, - { url = "https://files.pythonhosted.org/packages/2b/bb/e01b8cf906479b2fa046e992b84a9d9f39c1ed4058acc353004cd9a04df9/multidict-6.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2622fe114c0bd66ca5c461859357587f5a5e35ee5ff49fc5643d1bc78dbb41c6", size = 51008, upload-time = "2026-09-09T13:53:20.044Z" }, - { url = "https://files.pythonhosted.org/packages/c5/da/35d70c920812d9ddc6f295f6426457665194335fbc35aa0b96716aba219f/multidict-6.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:26a7aafc992e78872e2c8c1f7248c0e01139cf9020a7781b0c064fa566832712", size = 50232, upload-time = "2026-09-09T13:53:21.356Z" }, - { url = "https://files.pythonhosted.org/packages/91/6b/4c988a7c0daa4fbffc6080ed3c37b3a67cf225ba1de69d10a19ca1dd8d0d/multidict-6.8.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:05c2e90c5289c5f7436ba2c25812a5fbdaa1c1bc11c8d8d3bbf64f5cd7c633dd", size = 271678, upload-time = "2026-09-09T13:53:22.716Z" }, - { url = "https://files.pythonhosted.org/packages/73/2b/22c7de8a72fc5c36390e8049d86d842b032ac7c87ade035a3dafb7df4ffc/multidict-6.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7941ef106ca1f2c62314a13c7ed913bcf49641f3efdc12864d588e17870920ac", size = 270283, upload-time = "2026-09-09T13:53:24.235Z" }, - { url = "https://files.pythonhosted.org/packages/ce/f4/c1428318f945c57c016ba690338af41f87f18a7d3a7ef3227b1440a2c169/multidict-6.8.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a5a7ee1217949ddd43c6b7bcf70d5c22193bb50e8c695386de5905325e93ce9f", size = 245306, upload-time = "2026-09-09T13:53:25.656Z" }, - { url = "https://files.pythonhosted.org/packages/8b/92/a37f7519fb32b0bf43b0540292effe60edaf0691959214b227795bd3d56a/multidict-6.8.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6b62b7e0025aa48dec11e125e655d1157985a5fdcec04b1ad500101ad072b891", size = 279567, upload-time = "2026-09-09T13:53:27.162Z" }, - { url = "https://files.pythonhosted.org/packages/51/fe/a93c2ce417401863cc88ecf6561577625c140990d412e37f347a1c03a144/multidict-6.8.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ea880d441be7c510106bc56064be39266d948aef94ad4955e8784690019a5d9f", size = 282526, upload-time = "2026-09-09T13:53:28.927Z" }, - { url = "https://files.pythonhosted.org/packages/f2/9d/6bb4f84fdd82acfa09dc312ac133e7f76cbf2370004447f2a90e65e5d63f/multidict-6.8.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ab83fdd8cf307353edba9c427c17a3a021c2522d690f5633dd9f72d28b48ccca", size = 272604, upload-time = "2026-09-09T13:53:30.595Z" }, - { url = "https://files.pythonhosted.org/packages/3a/97/df0a30a4d786d313f24b39cb96edaaa3bbfe83a0b309577c81a797783ec5/multidict-6.8.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3d1f48582686a0a3b81e9b43234766cc96697df72081af3f48107bd3f34d34e5", size = 250786, upload-time = "2026-09-09T13:53:32.15Z" }, - { url = "https://files.pythonhosted.org/packages/6f/72/e59a917680d00214ba41f9fec19a8bec48f3bdf62656bc4377f37ae30947/multidict-6.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:563661919f603374c40cf45ffcd25535c12b8954203569a2ab1cee5265871cf4", size = 265419, upload-time = "2026-09-09T13:53:33.943Z" }, - { url = "https://files.pythonhosted.org/packages/47/21/0eb8868982ff07c1a2faaef7502ee0be32ef247dc1bf27881c51e7f4b20d/multidict-6.8.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:2ba9933e8f35fe4a70f540b837254c4055da82dc3a9e500a8f95e61498083a15", size = 258934, upload-time = "2026-09-09T13:53:35.662Z" }, - { url = "https://files.pythonhosted.org/packages/fa/a6/0586396716faf950c10ffbe733e4a57b4eeda9f7073e60c09bd4a05a766e/multidict-6.8.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:68d40b2bace413f3231f5729d3fcfb1837fd31c4907e241b5d43211bfd76f3c2", size = 273168, upload-time = "2026-09-09T13:53:37.131Z" }, - { url = "https://files.pythonhosted.org/packages/31/79/7197af20190d0d832be3b18582be46c224f64f5ba1cd35d32068d6af31ed/multidict-6.8.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a9e246f67ac038568b854ed7c5578e4c6af1f742359901a8fcc3603ff1358df6", size = 275884, upload-time = "2026-09-09T13:53:38.579Z" }, - { url = "https://files.pythonhosted.org/packages/f7/f7/af60573e25ffecc09e805464580d579338cf1a44332ab52757038435ed60/multidict-6.8.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:29b6e7bc4442a56cf8e0dc1cabf3fdc77cd533568d6829fc76a1effd2ce332ec", size = 246967, upload-time = "2026-09-09T13:53:40.172Z" }, - { url = "https://files.pythonhosted.org/packages/6b/02/4459f8c5025ab034d3d9af9a34bbde11319016cff2f327362c8ec43a80a1/multidict-6.8.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:82780eb8bf59e8fb25dd081fde6e058805045d6374a7f2f877effc826ca4434b", size = 272358, upload-time = "2026-09-09T13:53:41.868Z" }, - { url = "https://files.pythonhosted.org/packages/51/99/680d3522ab51a77094d31d7958c9f5989499a01ffbe5aebe11d25212b385/multidict-6.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2196ba6df392c3574acadd14ef87550f3611349c8618564de324b806a7a31cee", size = 267450, upload-time = "2026-09-09T13:53:43.646Z" }, - { url = "https://files.pythonhosted.org/packages/4a/04/d0c773805b0aea171287b01a82e4c28c59ef2c2d93e8047394765181363f/multidict-6.8.0-cp311-cp311-win32.whl", hash = "sha256:b8b7aa75146266fd3e2a2437cf69ae188688c04ab8665b163d4257b46c1e0c83", size = 46847, upload-time = "2026-09-09T13:53:45.161Z" }, - { url = "https://files.pythonhosted.org/packages/71/f8/1a959771a4dcd3224bd7bb40054f66b98ba5b20d6b74fd273f548f887e0a/multidict-6.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:b03ca066b47b18b205cc080dca6f76cbd159f8cdd33a02a0700164c13b37e463", size = 51549, upload-time = "2026-09-09T13:53:46.448Z" }, - { url = "https://files.pythonhosted.org/packages/64/7c/3a74b11599a9d8f3cfbb78b9c5cac3ff3cdc17278e4c00329c7c18dcaff9/multidict-6.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:54af1266710cb0f305127ae0b970aff8d208057f8a29cd6e1db99b0114947035", size = 48020, upload-time = "2026-09-09T13:53:47.767Z" }, - { url = "https://files.pythonhosted.org/packages/13/83/a4621577679149ea001806f5963f3fc687c391c1bd5217157be2278863f5/multidict-6.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:22a310ad37672a261e55a8b5e28d0ae08cfb68abb1f46418ccd19835c3b8e836", size = 84146, upload-time = "2026-09-09T13:53:49.163Z" }, - { url = "https://files.pythonhosted.org/packages/09/00/236b063f3e606055a3a9ba8faa5d40e6c688b059a58056b055f213476f46/multidict-6.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bb8c7da8c861391f7ae48e3593762be2dabe405109e01aec520fbe1a6d15d14b", size = 51049, upload-time = "2026-09-09T13:53:50.46Z" }, - { url = "https://files.pythonhosted.org/packages/91/9d/954b139bfa969855f2d4cb5ae7b7d44dd7106f754305b6e21a9068213aa7/multidict-6.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:10456943903744ae1249728161c96bd9d2f7eb5ee17fcc2ffda2dc32e1bb36c7", size = 49362, upload-time = "2026-09-09T13:53:51.878Z" }, - { url = "https://files.pythonhosted.org/packages/d7/8a/8774f5b3f6d5266ecd1117876e04b405f0f1ce19aa750b35a826efe6cfe4/multidict-6.8.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:658f5a1895b804423d97b22d06fc0d0b171c7c01dcc3aa9c8faf0c0e26a249a5", size = 278619, upload-time = "2026-09-09T13:53:53.44Z" }, - { url = "https://files.pythonhosted.org/packages/db/47/736080fec911ed9f2dd57ccab5a8145e4f17c4987de0bfc27bee20e4d170/multidict-6.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90c10b22860dbd09982d0b8993b66231a861bea2993d4a817ff35273f6ea285a", size = 283771, upload-time = "2026-09-09T13:53:55.048Z" }, - { url = "https://files.pythonhosted.org/packages/4c/d5/b7f41f59b0583f092602308a5e7c16ec5efd00d60214b22511e89a38dd19/multidict-6.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:088b04a66b3c1fce6fe4d771ec184a0426262d0b86709c908477b4ac7965df40", size = 262108, upload-time = "2026-09-09T13:53:56.631Z" }, - { url = "https://files.pythonhosted.org/packages/54/b2/a52dc06c6e2598672308e3d392fd85b837b23c25dda459bedaea84985080/multidict-6.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9caef53b20a105c0d66518a34be2f71b2783de8d091767575ef86f6ea422236d", size = 289899, upload-time = "2026-09-09T13:53:58.415Z" }, - { url = "https://files.pythonhosted.org/packages/a9/21/00cda7983f37d119b86f1f89d5b4cf771ecb6d0fedeb9a0971758d6d6d4a/multidict-6.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a5e1583c14775580da05641240ce0d93f36ce3ddef3d5083a827468b0bcfe874", size = 293025, upload-time = "2026-09-09T13:53:59.973Z" }, - { url = "https://files.pythonhosted.org/packages/c6/c7/4544cc02e45bbfac4d8788b05379bb360021fd8c53fa74b0f624126ac188/multidict-6.8.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:003a3bddb32915c3f67096ea41d24e53edf710edb65a1f5d0c70ab40b0e4d20b", size = 287410, upload-time = "2026-09-09T13:54:01.652Z" }, - { url = "https://files.pythonhosted.org/packages/43/1a/7abed90b8eba381842235bfa6f4d730204fd7deb374fc87e3ec9b2c2b4ac/multidict-6.8.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:35977263d9bf506dbc65349f63b3b8c91606d4abc110990945e3b94bc671319c", size = 255878, upload-time = "2026-09-09T13:54:03.366Z" }, - { url = "https://files.pythonhosted.org/packages/25/3e/73fae10e15fc4d711975337caff7e494c87de5d0189afe3518b21b945326/multidict-6.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e9dc7b4ff6ef184504b49ef9a4113d49a646653b2ce89f5f48c1f57cdf6ba081", size = 277831, upload-time = "2026-09-09T13:54:04.963Z" }, - { url = "https://files.pythonhosted.org/packages/c5/cf/01cfc81492933331147004861bdff201d8adeba8485ecd8f490e755fe7e8/multidict-6.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:887f9a975996032c686719eb7b3e1e7942fab5079c2b778bbd9afe9a9d78244f", size = 275096, upload-time = "2026-09-09T13:54:06.661Z" }, - { url = "https://files.pythonhosted.org/packages/de/59/e9a3773b17297fa1e38fd4b3c6f5f2f458380796be62eca7d0d77c250618/multidict-6.8.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:f3071e6515cc63714d014da8f738ae9fa3997c476203f3cd46de380c2376ed7b", size = 279803, upload-time = "2026-09-09T13:54:08.389Z" }, - { url = "https://files.pythonhosted.org/packages/64/9d/2d712a2605b3971908e3b4f5eb6f98c353d9991e106f684d0e08ae581814/multidict-6.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c5f3a2af441670d80ce5fdf13b6c1b421fc1fc7fc5182d58ac7486738bb2b742", size = 284595, upload-time = "2026-09-09T13:54:10.17Z" }, - { url = "https://files.pythonhosted.org/packages/58/6c/21aded8586e552b29892268c576e5745d1a894c5451c9866ca3c06b7ec50/multidict-6.8.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:31e8901637e20ccb3cf8f8848b5d0f7a00462bf5b34f7cf3dcbb2753b18e8b39", size = 252641, upload-time = "2026-09-09T13:54:11.811Z" }, - { url = "https://files.pythonhosted.org/packages/2a/70/56a415ae0a45e5eae2ec817d46aeb72a1ae777863621c85f1f39d329275b/multidict-6.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:13967dca8b2f33230a1427b52438326bb1c9101a1df22a3309ed3fcbbb3c96f0", size = 283369, upload-time = "2026-09-09T13:54:13.59Z" }, - { url = "https://files.pythonhosted.org/packages/08/7e/7b7cd611fd94bf2f6bd16244c50495867ba394d5baaf8e6e487d39494ab3/multidict-6.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad474c11d851b6fc97cb625e4822bc0cbd567fc07dc2602e28faec5a36b42bbb", size = 281653, upload-time = "2026-09-09T13:54:15.174Z" }, - { url = "https://files.pythonhosted.org/packages/33/4a/b19a5892ef2ef6c68ae278b4f1504b82e01037baedd92c55d37e55ecad00/multidict-6.8.0-cp312-cp312-win32.whl", hash = "sha256:7bb0dad75068fee80fcb60f88569722c199d8656a16706702dc6e3b786819c90", size = 47936, upload-time = "2026-09-09T13:54:16.638Z" }, - { url = "https://files.pythonhosted.org/packages/29/00/1952f9f282aa71e7c3db3a6b47afb689d0ddf283dbded7e6326a91d421c9/multidict-6.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:7d26dc8f070c0ec5579e987fa615ffd6883086106eefdff9e10d160fc5630630", size = 51723, upload-time = "2026-09-09T13:54:18.05Z" }, - { url = "https://files.pythonhosted.org/packages/49/b5/c9d57dbafe25b8f3460ce2961c968539a81ff7a70160c44dcfd4255cbcd1/multidict-6.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:e6ec7d37841609a691b96a10b4fde386c7cd93ebbb939f59c9f23325ee788395", size = 48492, upload-time = "2026-09-09T13:54:19.42Z" }, - { url = "https://files.pythonhosted.org/packages/84/1f/d7112c2dd7db02677097be72fb65542f51a5aa73cb472b87ec211ba9e0dd/multidict-6.8.0-cp313-cp313-android_24_x86_64.whl", hash = "sha256:ec0a4d066356054d569a66e0a94691a2058b680be5e710298f61db11a3c4609f", size = 54197, upload-time = "2026-09-09T13:54:20.814Z" }, - { url = "https://files.pythonhosted.org/packages/ae/24/876015abbcb4a179d946579eb77b778eb5a948fc8381bc7928ba895bc051/multidict-6.8.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:714597cb5d5e15a8a449d2ae23c45b486a9e8fa33c462c7a33d7f35b65d92943", size = 47787, upload-time = "2026-09-09T13:54:22.51Z" }, - { url = "https://files.pythonhosted.org/packages/06/c1/ceb7d25f8a567599db2eb19b08cac58d67ff553cff42dcadbea9aba56a20/multidict-6.8.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:e0db3a4d1e264e225037a6023888972c25206a96e016021a5bea41c9a939f2a9", size = 48815, upload-time = "2026-09-09T13:54:23.986Z" }, - { url = "https://files.pythonhosted.org/packages/18/e3/e1c6e9c3818c34b782f23ce5fdba3eaa34ec6750dc53078dfac80fa59be7/multidict-6.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:27747162712e85c84598d364425dbf1714ff335bdb6ba3171c4e5081196e8916", size = 83484, upload-time = "2026-09-09T13:54:25.674Z" }, - { url = "https://files.pythonhosted.org/packages/4a/a0/c23f78a4badee9a5b3e760495c661c62a92c340a1dfd00f829cd16e256bb/multidict-6.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:847d6082ae694dc95e548acb201bc100e1cfa96513bc71fdcb86f709dad6c435", size = 50763, upload-time = "2026-09-09T13:54:27.135Z" }, - { url = "https://files.pythonhosted.org/packages/c4/fe/db552d402a3f6b650f5d3ae11b82b93833836aebb51bcda22d8691121129/multidict-6.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:88a6df88567680504ae28bfa7a1f2f64243d91e79a40b2c92ef42efc531e23da", size = 49029, upload-time = "2026-09-09T13:54:28.483Z" }, - { url = "https://files.pythonhosted.org/packages/01/b4/546853fba19dcef77cdf91fc173faf0b02284a49106cf250511166b4ec5c/multidict-6.8.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:560b211fc3bd4a1e1c6de44f6d38113bf5b410dfc89a4c0d2a3c0edbf1a0dfb8", size = 278863, upload-time = "2026-09-09T13:54:30.145Z" }, - { url = "https://files.pythonhosted.org/packages/ee/3f/4b52dac7db547936eb762123ac1d99df23f92fdb358bae600e322f611247/multidict-6.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:202436df907c15adbb94360296c425ea53cf8968a5d2cff9b5b9790ae1972b33", size = 283915, upload-time = "2026-09-09T13:54:31.937Z" }, - { url = "https://files.pythonhosted.org/packages/fd/6e/c0dfbf170e49a91bcb9ce850d51cb98357f3033c5227529200ca7625853e/multidict-6.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c46a08bf070d6849fed483e9d9833f9d06aecb8382ed985be0b38508b3ae958e", size = 260704, upload-time = "2026-09-09T13:54:33.529Z" }, - { url = "https://files.pythonhosted.org/packages/91/02/56973a060ab8dfc2e80bb6797682f6577aff7123cdb1de1a568670ae3499/multidict-6.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2cd560498ae8e1bcc955643c1d78eb8e338226d07a983c656ea8c4443d3eec0f", size = 290243, upload-time = "2026-09-09T13:54:35.408Z" }, - { url = "https://files.pythonhosted.org/packages/d5/67/69112989f131bdea4a87b74e82cb0a2daf37880cd92b0e6f0420020adceb/multidict-6.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:758233648ac47b07c575224c4eadd73c8929c3b4c31e2afcfea935fde1cda735", size = 291131, upload-time = "2026-09-09T13:54:37.205Z" }, - { url = "https://files.pythonhosted.org/packages/c2/75/9435f68b0cfc442d4917de85c26f2b2e1292630883414a25576083fa2469/multidict-6.8.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:122adc7c46ac1e31ecfc7f81b2530533dccafdba70f5d741649f87e336c63384", size = 287551, upload-time = "2026-09-09T13:54:38.835Z" }, - { url = "https://files.pythonhosted.org/packages/13/08/2ee4838081d6587849611aa7ec722c4cb2469e912fd0eaee980e7bac064c/multidict-6.8.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8125e60f3c70e323ac07dd8b3635f7b3bbc5c3a9ac04ae5988f668ff7ae28a18", size = 254591, upload-time = "2026-09-09T13:54:40.806Z" }, - { url = "https://files.pythonhosted.org/packages/94/f1/05673b51191f77f4198b8e4b35f16ea71c0300c72ca8aa027a66a61b6edc/multidict-6.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:83ff054b04915be5c15680da6c6012474a2cc2bf534129a0e8c6a99f17ba7238", size = 278204, upload-time = "2026-09-09T13:54:42.672Z" }, - { url = "https://files.pythonhosted.org/packages/45/4f/b6cf74322b3fbd3e011a1e903730191922291a7779f6d404114c2189b806/multidict-6.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:930c6058047410e3edff445f5a6e4457f2e089042dede00e2d18ce06f3ceae2e", size = 275600, upload-time = "2026-09-09T13:54:44.348Z" }, - { url = "https://files.pythonhosted.org/packages/1b/ab/958bbb04377159ff03c7314cd9d8a48dd6fc4f78c840589c22ab155ee9c7/multidict-6.8.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:13e26f59f0eecfc5f67c663ad550ffdaf62c0f657547cde387f6c86af1c9449e", size = 279793, upload-time = "2026-09-09T13:54:46.086Z" }, - { url = "https://files.pythonhosted.org/packages/a0/3a/706605ab0dfc4179748ee7949829e63c6f14ae28667aceeefaf2c701807f/multidict-6.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fd789a294d8e098528be29b2669b83005ce569339f8cef167fc0274c3115c34c", size = 284751, upload-time = "2026-09-09T13:54:47.793Z" }, - { url = "https://files.pythonhosted.org/packages/b3/a5/567e36c013ad023546de633079c6b22101dd43226b193cba00e6399703be/multidict-6.8.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:3126f2a96704505aa4e92a72d6e8a5d7f29d40a987ced8bf69e29d71dfc71fbc", size = 250812, upload-time = "2026-09-09T13:54:49.509Z" }, - { url = "https://files.pythonhosted.org/packages/0d/5f/6b0b64aa0cd346b07831dabaa6ccda0e73014c5df044b68baa763f0f0552/multidict-6.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:23c9ee89967b6a9b4048acb3b93b660ed714ce9c8bf3bbe652959bc120dc02dc", size = 281606, upload-time = "2026-09-09T13:54:51.288Z" }, - { url = "https://files.pythonhosted.org/packages/31/8c/b846b6796f26d496efb07fedef2b69f6de533da32a56f12d236722a96157/multidict-6.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7a62e302fc8cd6aa8972207e7e951d1fdee7c1dda18568305041d19f0e2c00f5", size = 281733, upload-time = "2026-09-09T13:54:53.05Z" }, - { url = "https://files.pythonhosted.org/packages/f0/f3/bf14a39d4af5697fd9404baaf70a0aeeb82d258b95de5cb16b1a7f98ae6f/multidict-6.8.0-cp313-cp313-win32.whl", hash = "sha256:093167d22a8c95af30f597b8a5686f20a14512989942d4be804d119899caca20", size = 47738, upload-time = "2026-09-09T13:54:54.676Z" }, - { url = "https://files.pythonhosted.org/packages/19/0a/598511a5741a3cb374971b3b02eda8a09896118ba528a54795f7e7e8bfb4/multidict-6.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:f25b61a708bd276e8cbb6afcbbf1b8e793a3be70ba0a842d0b8692020f83b706", size = 51609, upload-time = "2026-09-09T13:54:56.38Z" }, - { url = "https://files.pythonhosted.org/packages/fd/b7/6f5c1bd4ffe42d4a6db0f2f65491d4088e9c25c990358fb31a614621d664/multidict-6.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:bb36381e1f9f9d06eba2f10bdd438e5d20c07d5b55e1a3eee30b9f44cbf52316", size = 48280, upload-time = "2026-09-09T13:54:58.03Z" }, - { url = "https://files.pythonhosted.org/packages/ab/85/153341590e233a967c1d6791a83402d01693dec0f4c1f695606ef16c7ed2/multidict-6.8.0-cp314-cp314-android_24_x86_64.whl", hash = "sha256:f8b09b25e0f4dc2ea9e2adbb1cc3ba11a94d6fa3dd978ae659c8743052e1afbc", size = 53758, upload-time = "2026-09-09T13:54:59.563Z" }, - { url = "https://files.pythonhosted.org/packages/fd/ff/44f72d516ece0398683ef52061797d83a74b16b8c1e4587408e97959d783/multidict-6.8.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:1f57c414be82490bc0e0305fdb834186229b2d9b6a35fa0afd1eb1a772d125ab", size = 47495, upload-time = "2026-09-09T13:55:01.382Z" }, - { url = "https://files.pythonhosted.org/packages/50/5f/6e118f761b024dd35d26c2fe7ba41572bb0e8ac5f8cfccbbcbc2ff76da4e/multidict-6.8.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:00be37bde741bf60871082cd347a093218c44886e99231b7516671c70f2c280d", size = 48540, upload-time = "2026-09-09T13:55:02.989Z" }, - { url = "https://files.pythonhosted.org/packages/e8/4b/3eed744491b32f0e318e7db89dc06858732362f706e8d045fa9ab51a343a/multidict-6.8.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:e37b744849fb631bb52e3dadde35ffeee365a6c41cf71257b5b7acc9cd83fd38", size = 83130, upload-time = "2026-09-09T13:55:04.554Z" }, - { url = "https://files.pythonhosted.org/packages/6d/de/95c2c0ddcccb9a41ffbaa5df8ea059a8ff81916b7617a8847ecd89ed8061/multidict-6.8.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c2b2a96cf1dd99fe7867be4c013314225f4d5786e6685906e29932d42aca6f11", size = 50574, upload-time = "2026-09-09T13:55:06.387Z" }, - { url = "https://files.pythonhosted.org/packages/f5/b7/f4f4989594f99bc121ad9277090c4e49819b08ab1a96e132b628a9e10b7d/multidict-6.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:bea7df027015856ba5d0a88e3b4777ff8cb5c66b58fc108050fe79d4dd9d4d2d", size = 48786, upload-time = "2026-09-09T13:55:08.131Z" }, - { url = "https://files.pythonhosted.org/packages/b2/86/f1d86a0222f31fb3df8eef3d6c9abf7e8d65d49edd8d0d7e7afaf23d23cc/multidict-6.8.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d3da668e903c934ed0b587ecacfed6901f6ae6384a6e975887592b61845e78bc", size = 276670, upload-time = "2026-09-09T13:55:09.803Z" }, - { url = "https://files.pythonhosted.org/packages/03/50/6945c50f86a978b2bcace9ca344165ff80883be47d984489bbba8fa0ab20/multidict-6.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:64eaeda36ee8d88f9e8616a587a8c66a663283cf6e0dcf013c1ddd8c758e4aef", size = 279339, upload-time = "2026-09-09T13:55:11.685Z" }, - { url = "https://files.pythonhosted.org/packages/ee/2c/e649889ba23fd1f4442a85427b99d9e6261226b2ac31914aa7f5b241d947/multidict-6.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ac746cb365bac1c462da9e3e6ab8904a8efe2217a56b0b2e3d9480f41d2b2602", size = 252549, upload-time = "2026-09-09T13:55:13.527Z" }, - { url = "https://files.pythonhosted.org/packages/e5/f8/1023b66e011b1395fb160dabb0f0608ef67e569f0bdb2c1d5ac9b2f2adc6/multidict-6.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:18f0e06360c3e451a3ab800355773c8d125a758238d780c800b0ee5e90ee903c", size = 286203, upload-time = "2026-09-09T13:55:15.19Z" }, - { url = "https://files.pythonhosted.org/packages/7e/6c/48aea545cbda6d0444848ec23d988c13b86538a00a1b7d3868cc2382ff94/multidict-6.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:69708fecaa88bcb2341397b49fc95057a835b02a3670c551b37f95dd79e64e3a", size = 285039, upload-time = "2026-09-09T13:55:16.928Z" }, - { url = "https://files.pythonhosted.org/packages/68/2a/066123b17291671bf67d2a5c65ee81a48de53913bd1b1578791519eacdb0/multidict-6.8.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9606f583e7acaf61e7b3f56074e14037b9af7cb194590edfc0114b3ae5931ff7", size = 281075, upload-time = "2026-09-09T13:55:19.155Z" }, - { url = "https://files.pythonhosted.org/packages/47/20/4f0b2c485da2e8a659cc677717a3745872918c9c85064491a1ef75d7a3bf/multidict-6.8.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1f66fe6a021173d0d47968491791966b9f3e6d61115f2491744aa0c07a6e67af", size = 250431, upload-time = "2026-09-09T13:55:21.07Z" }, - { url = "https://files.pythonhosted.org/packages/ff/c7/b9a288901577aa0b82c33c64d52246c88076d260ad7b6c16b021ca0f8e99/multidict-6.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:be007d1aee2cbd530347dcafedb400891a3b5f1bd7135f95cf5d5b330b5219ee", size = 273891, upload-time = "2026-09-09T13:55:22.887Z" }, - { url = "https://files.pythonhosted.org/packages/da/51/0ba50cab2cfd067988de2abb73f23076ac727fe18d03f1368a59def64727/multidict-6.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:8457aff3c12a89a8e1c4674de5c777857fbc429f40fe117a3d29538547cbc364", size = 265262, upload-time = "2026-09-09T13:55:24.77Z" }, - { url = "https://files.pythonhosted.org/packages/0f/d6/e5be1117dbca6eb9ce231142b7e20599418bb3500147db51bf844ce8afcb/multidict-6.8.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:95c27b4f3f04320fc44e338573f40c5c956b504a7fcf081a157fd0b02579311c", size = 278033, upload-time = "2026-09-09T13:55:26.67Z" }, - { url = "https://files.pythonhosted.org/packages/d2/28/cad0afaec3caa56ea2c1ceed43c164d62ad3e83e950daf0d0c87bcf9dca7/multidict-6.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:d244cf6b52b5ba1c34c3832f4652a668ebb36d95949b96eed9a1c54d916a90dd", size = 281717, upload-time = "2026-09-09T13:55:28.569Z" }, - { url = "https://files.pythonhosted.org/packages/c9/d2/025702df0b69b856db70a4d66f77622f51c3d99771ec9a07f3ca80f7e098/multidict-6.8.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5bbbb696c8024475b1877d14ce20d5f1cc05b8f6d786cea0fe3aa7fedc02e891", size = 247124, upload-time = "2026-09-09T13:55:30.497Z" }, - { url = "https://files.pythonhosted.org/packages/b4/96/9dddca563f06a921956389c0bc9b894355b98b0bdf62299e2560c50afb6d/multidict-6.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:cbd86f9787c5e2f5fd27d8b21458222f107347c6731c4e93dde68f554b466a2d", size = 275954, upload-time = "2026-09-09T13:55:32.57Z" }, - { url = "https://files.pythonhosted.org/packages/ec/91/8b2f1f2a774a955665f268340a2b59db7020c5f12baac02ae9ef1b1660cf/multidict-6.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2f8a4b0b4d639d525928c7f30de527bfdf9ead6e44a5e8cb9c50aced5e4590cb", size = 275508, upload-time = "2026-09-09T13:55:34.368Z" }, - { url = "https://files.pythonhosted.org/packages/b6/1a/e2cabdfc0880a61a99d2b8bc361035036fb5a2c6af31ea3fa054ba1065c5/multidict-6.8.0-cp314-cp314-win32.whl", hash = "sha256:8890c89d662560e51c55ac1304d6f919b23942abe9ae1127cb1de9aa6132fa52", size = 46938, upload-time = "2026-09-09T13:55:36.057Z" }, - { url = "https://files.pythonhosted.org/packages/b9/7c/11234bcba62c22a58f2ba168499cfe3531f49de3edd5090d04a8c6cdc936/multidict-6.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:45cc39ba50fb0754a4359b90f8229ae08598fe2266abe3521b4e5a9ba916534a", size = 50291, upload-time = "2026-09-09T13:55:37.698Z" }, - { url = "https://files.pythonhosted.org/packages/ab/61/793668439df924752a8137d6db0de97ed1add494779b01e4764dfc60571b/multidict-6.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d0264f8d5cb0a803f650a6a8572dfa0cd1e099a2234c588dc8fb220b415b865f", size = 47622, upload-time = "2026-09-09T13:55:39.335Z" }, - { url = "https://files.pythonhosted.org/packages/9c/b8/3c091b929e6b5b2f6e0eba2232178e76d4503c8b96b92dfc281ff1d823be/multidict-6.8.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:1969971900b0871530f9b62280dcc2d75688e74d2a69262bc01faf2b96c78f04", size = 88789, upload-time = "2026-09-09T13:55:41.086Z" }, - { url = "https://files.pythonhosted.org/packages/9e/d7/3df83fab22dd64615db71e3b3cc1346b581d1459719637ce52144f9f6558/multidict-6.8.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:8180b635290a75af8478f1b3e9810135381ae24833293fe77b85c1c21ff842ab", size = 53399, upload-time = "2026-09-09T13:55:42.685Z" }, - { url = "https://files.pythonhosted.org/packages/2d/78/41bd04c04b0aed16540c4856c9e012afc1c254298da154398308df05e26a/multidict-6.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:4261863fc8b5ab1b815ede94e592e94c6af5b04616014929057e61859e7382a9", size = 51597, upload-time = "2026-09-09T13:55:44.569Z" }, - { url = "https://files.pythonhosted.org/packages/2f/6b/7bc4cdddf624e1e7e0231734b1331729ea46df10d7c8fd3fce79756e7d0e/multidict-6.8.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0b143d53590e89f43153d81d505a8448d4d57354354385aef8a51d67ffefa27e", size = 264391, upload-time = "2026-09-09T13:55:46.548Z" }, - { url = "https://files.pythonhosted.org/packages/dc/b6/d2a946e5938771e92c39354563e535ef6bc6dfe399dd4307c6df8dfea183/multidict-6.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:da1c112c5784ccd9d32cd90be6739fee32644e874eff6ae8f0497cba3e352e58", size = 264680, upload-time = "2026-09-09T13:55:49.915Z" }, - { url = "https://files.pythonhosted.org/packages/33/6b/3f9e981c42e7eb9329918523f0f9362ceb0ac3ee0ee1165c28f674249d75/multidict-6.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f7eefd0233a7c33ca980a5cfef26f1e9b5e2137839e752a99963696729f12d91", size = 235420, upload-time = "2026-09-09T13:55:51.92Z" }, - { url = "https://files.pythonhosted.org/packages/bc/e1/a3a33a039fb6d381800ae5d1d587b697b8c27fcdfe48819420f08703acba/multidict-6.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:348bb85e2038b40c007383616d73f734869063772372519549ebd7da1723d1a4", size = 270309, upload-time = "2026-09-09T13:55:54.023Z" }, - { url = "https://files.pythonhosted.org/packages/95/5d/8b06724a957f2e480f159b9550988a67810fbe9555a09c5f6a2a4b829607/multidict-6.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:095f62ea4e7a3be2f6c567ab695ce10e950f2adb905c1bec82281593e0b2d2ad", size = 275169, upload-time = "2026-09-09T13:55:55.948Z" }, - { url = "https://files.pythonhosted.org/packages/ab/32/8f3dfe2ffa5d0df2a95f71e63c2f11fe3b5e1771f26ef73bb1af84de83f8/multidict-6.8.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:be569fff1d85cd29391c431c5641c8772acb75bbdc61e60a8e82fceb9023d385", size = 264900, upload-time = "2026-09-09T13:55:57.803Z" }, - { url = "https://files.pythonhosted.org/packages/6b/73/d5829fc00a055d6ab445e0876346ee9cdee670766cd4190dc0a496188c0f/multidict-6.8.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3533a03e4e789baf6a286e7b0b1b6da3f3d7c3eab569686ee29ee1d8b52e2cb4", size = 242486, upload-time = "2026-09-09T13:56:00.002Z" }, - { url = "https://files.pythonhosted.org/packages/b7/58/e8d7874038e31e0533182d1c3c5331a856b9c849a71bb26a21850e8c91e1/multidict-6.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1bdb9b8fba5a9aef673ec90db3f55b1ce743f2fbdea4d37dc04d14ccdfc153ff", size = 259916, upload-time = "2026-09-09T13:56:01.802Z" }, - { url = "https://files.pythonhosted.org/packages/4d/f8/1b56a7401acda20efc016440f4fad3bef66c4aee54ca080ec143881ebb0d/multidict-6.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:f8d7b66c9e09c0bb0add2b5895e646b62a0849e71155066f215523de6b95cbe6", size = 251209, upload-time = "2026-09-09T13:56:03.767Z" }, - { url = "https://files.pythonhosted.org/packages/bd/5b/68d67a9e302b0645a747ba910c30eb41f2834fcdc1d85f53eae2dfceee0a/multidict-6.8.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:563d6500ca80dac7bba6f48a78e0ffd87e21a7d4d24642c6503a2ddccd70c110", size = 264505, upload-time = "2026-09-09T13:56:05.795Z" }, - { url = "https://files.pythonhosted.org/packages/18/13/4dc304ba2c5f5307b474ab2ce1ed1f6b02b0b4e233c182e3981ed436c2e3/multidict-6.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:346ac52e56bcda320c0dcdfdd081947ed7cada33afea4e2284bef7b0733bff9b", size = 264916, upload-time = "2026-09-09T13:56:09.079Z" }, - { url = "https://files.pythonhosted.org/packages/dc/0f/7b1f729d18369915009185201be5d0b8df0e525340fe6a600d2f8441d6cf/multidict-6.8.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:4ee953a5ebaeed38dc21cc032ed17a9d9782802e00042200497ab4b01b0bf7c0", size = 236839, upload-time = "2026-09-09T13:56:11.273Z" }, - { url = "https://files.pythonhosted.org/packages/22/d1/eba1b88b18b7019d9136303fe77909257c40fabde5aaf138a4d900b6ce3c/multidict-6.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:2f79cc3e8039a8cf5c77e0811b0807953fd52d0863b9b76970b20d696dc64a78", size = 265307, upload-time = "2026-09-09T13:56:13.379Z" }, - { url = "https://files.pythonhosted.org/packages/aa/a6/6c1e4106faa27118ac612f4d664eaf909de252634785286262a627108e58/multidict-6.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:43a4b56555bbcf8af161e7c7682bd93eec10f068c95844511864c018c8e5e13b", size = 259041, upload-time = "2026-09-09T13:56:15.666Z" }, - { url = "https://files.pythonhosted.org/packages/c0/bc/ecfb8b6faa8e158a71b03bdf7f947f30e0bc5d899cc357573a76ab7bb1e5/multidict-6.8.0-cp314-cp314t-win32.whl", hash = "sha256:48ea524a25a1cd5972cf293bc95713918cba0bcd6fa9b992d906c857c546abe2", size = 50628, upload-time = "2026-09-09T13:56:17.837Z" }, - { url = "https://files.pythonhosted.org/packages/30/7f/e27fb699b70ad24dbd02ddee604658acb36f907c03c045baffe4ea774501/multidict-6.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d0be2b832435001bc623ca7f1499ca1a853d4f082fb61221a80ce71132f50b26", size = 55592, upload-time = "2026-09-09T13:56:19.652Z" }, - { url = "https://files.pythonhosted.org/packages/eb/7a/76de70b2f6733696803f1ee56abe44a3757a52777383032c7373d3fea0f4/multidict-6.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:62b8e291a4f7edbf7cde7a43d831d893ba443a1b627498b53581943b0e348feb", size = 50300, upload-time = "2026-09-09T13:56:21.516Z" }, - { url = "https://files.pythonhosted.org/packages/ce/32/4de7320ae032dc768090d11f708d2d386df3db04cb6b8b0db0230cfc66c3/multidict-6.8.0-cp315-cp315-android_24_x86_64.whl", hash = "sha256:e192018b732f7b168e6604cbdf40fa8e05c996693b9eb445a0d8a73f4b77c5d3", size = 53761, upload-time = "2026-09-09T13:56:23.192Z" }, - { url = "https://files.pythonhosted.org/packages/5c/45/ecb641309dc2cdc6040f18e22c68eb5e94398f9404c4365d810f4292e053/multidict-6.8.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:b25426f9f6ed402835617c8f23609a47045f91ecff365eb6734817e039a8ed25", size = 47505, upload-time = "2026-09-09T13:56:24.902Z" }, - { url = "https://files.pythonhosted.org/packages/eb/68/87d6161b9fef11943e0b894203da3fff561933ca3c9b2952b6e7100e9c9f/multidict-6.8.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:fa6c2880709c84457de104385b704fc28860f27e442ad13966fc4af8e714fe9c", size = 48549, upload-time = "2026-09-09T13:56:26.574Z" }, - { url = "https://files.pythonhosted.org/packages/97/f7/d852d2276407640cdbd29fe11cac6e93f70f59542cba174ef9d146738946/multidict-6.8.0-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:eabb03dc3e4ed6333ecd1cc9826ec80e7a98b5506deeb832d7260c8e44166d23", size = 83157, upload-time = "2026-09-09T13:56:28.227Z" }, - { url = "https://files.pythonhosted.org/packages/14/e3/16fe7ffa6090591d83cf6bc2486e77ce891705fb6d0191823140928311b5/multidict-6.8.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:59e539c4eb4d3a53b0e630a6ba2b2f2824732b5e73f90e30a280f12fde157b15", size = 50578, upload-time = "2026-09-09T13:56:30Z" }, - { url = "https://files.pythonhosted.org/packages/d0/0c/e38e41c1087a599f86ff58a01f358abf7c4db3c26a3e90eebb3e02193ef1/multidict-6.8.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:835d5a90b11d1f5f8200ff3cc8316bded76eebebc92436398947a27657e645e7", size = 48815, upload-time = "2026-09-09T13:56:32.056Z" }, - { url = "https://files.pythonhosted.org/packages/d3/f0/eb691f42af8e7775992f57904ec75dc356fc7cdc896e5f30879decdd26f2/multidict-6.8.0-cp315-cp315-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d2d236b8a44ae91536a12ebcb996bdb31cf27425f36b4d05c87f2ba2716050ba", size = 274804, upload-time = "2026-09-09T13:56:36.741Z" }, - { url = "https://files.pythonhosted.org/packages/87/05/28472ccfeb43c00a043c0385ca4294da21a5957859fb7860e2ebdb3e3011/multidict-6.8.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bb9a60b7faa5d37c426fa91cf4d6738182a1f2755b9fab7c9c64cd466c4ce51e", size = 279693, upload-time = "2026-09-09T13:56:38.531Z" }, - { url = "https://files.pythonhosted.org/packages/f3/a1/2b4fe73e5fecff807b47650a155c391a103136428cb21d6ba8e39c5912b5/multidict-6.8.0-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0ef606c15cac6c90279acf34120784b6f36662cbf382defd3955cd8f1115336b", size = 254969, upload-time = "2026-09-09T13:56:40.407Z" }, - { url = "https://files.pythonhosted.org/packages/44/e0/c97d1822783dfe52e02fd150fa3f02eb22410211a9e2615f71541803ed4b/multidict-6.8.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:68186a2d4051c8ffd17be33553bea2ec9bbc8ef860fe2980a221d96126296f31", size = 286392, upload-time = "2026-09-09T13:56:42.234Z" }, - { url = "https://files.pythonhosted.org/packages/2f/d9/772f1339e1d051236bcc137b0eac2b4aaaa0bbb56aaf924e9aaba901d9c1/multidict-6.8.0-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2cc66abb85e2108c9ff8a1c0d20fa260bf690bbb33caef4ff3ecb2c2cbdfff5d", size = 285348, upload-time = "2026-09-09T13:56:44.255Z" }, - { url = "https://files.pythonhosted.org/packages/06/ae/cd045747e4680362e02955a82c468e95b5e4d319e3a79574b3fb677de568/multidict-6.8.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:69b3e519a132bb943b0daae15fc8c2168706b17f826481d32a32a5e784b129e3", size = 282721, upload-time = "2026-09-09T13:56:46.088Z" }, - { url = "https://files.pythonhosted.org/packages/35/14/0802d9a3aae4ef21eaa39adbd729a380fa095932105e1424e417b53e783f/multidict-6.8.0-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e41226ecf607f062fe34a2f4cf64ad3a89e3a0180dc800b463b6b14c06dd10dc", size = 253168, upload-time = "2026-09-09T13:56:48.07Z" }, - { url = "https://files.pythonhosted.org/packages/b1/64/3f92298bab8fbe1332e708863fb55b66e755be6f416b3459720d48b33af9/multidict-6.8.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:610c7637bc36b90f39e6c66f710f93d57018f83d53e1e187caaa218c6892b95f", size = 274209, upload-time = "2026-09-09T13:56:50.023Z" }, - { url = "https://files.pythonhosted.org/packages/08/c2/2001ac0eac1a8b7390a5902d7115f66d4f256268057a502200b6ab12dad7/multidict-6.8.0-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:65c85c79f5a2c04fbbc18f006c014674dc5fdf270cb978d8862c82c6f694e60c", size = 268044, upload-time = "2026-09-09T13:56:52.033Z" }, - { url = "https://files.pythonhosted.org/packages/0d/90/78a9e26c85f89abd562a67f7fcbaef9007fd5c37bb9efac19f1cf604e7c2/multidict-6.8.0-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:628ff11e6720f90acd0c305dfa3339f04a783a20de8cda6ac333ba46447261e8", size = 274806, upload-time = "2026-09-09T13:56:53.975Z" }, - { url = "https://files.pythonhosted.org/packages/3d/71/713bd445421b21531234c1f3630b768192cb9d80c8b1c5b05c5b505ff4c0/multidict-6.8.0-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:0935971bffd0b479fc90c4811ca787703e93fcb6afea939a375dfc80285ab368", size = 281890, upload-time = "2026-09-09T13:56:55.848Z" }, - { url = "https://files.pythonhosted.org/packages/de/a5/1387c538663e2dc8c27bbc7cd6955cb66de0f55c780cf7cd0fc06a1a16ca/multidict-6.8.0-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:9442b14eec262a1f74369bbd07e75bc5155105164649a4b9fbc1ebc7b8fb0b14", size = 249749, upload-time = "2026-09-09T13:56:58.01Z" }, - { url = "https://files.pythonhosted.org/packages/91/15/104296c9d70896b9759ce0812aa4899fab76d8b16bb32dcc5a78ab547c89/multidict-6.8.0-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:397599503b718f0137f26d3f6532d6955069cd2e5917c47ef581495bc2529ff8", size = 276138, upload-time = "2026-09-09T13:57:03.591Z" }, - { url = "https://files.pythonhosted.org/packages/f7/0a/f2a0c2658e9d7ff5964ec2820a02054558636fafd663230ddc8310b8ed39/multidict-6.8.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:9e37024b41d7a7e7e9cce14b248d54707c21c2a2ea30a47b71bdcefcafec00f2", size = 277077, upload-time = "2026-09-09T13:57:06.024Z" }, - { url = "https://files.pythonhosted.org/packages/98/50/bc46566caffba5c1c4a510519156371edf7c4ecd35c9ef917d0c1803487d/multidict-6.8.0-cp315-cp315-win32.whl", hash = "sha256:071da134651b04a8507dfb331ac0988f376337c2aea59486bf20989fb5b5a64e", size = 46930, upload-time = "2026-09-09T13:57:08.009Z" }, - { url = "https://files.pythonhosted.org/packages/6f/1a/cafb31049ecc1a6ce52bcc69fa436cca239adc057b1718a0c49044848663/multidict-6.8.0-cp315-cp315-win_amd64.whl", hash = "sha256:3bafff8598f0528017ddc74194e5451d5c22d046c98935f8f86247b0f286e4f8", size = 50294, upload-time = "2026-09-09T13:57:09.986Z" }, - { url = "https://files.pythonhosted.org/packages/6b/51/00e037da14cd1d894b123e0bbe62de5c561679a6ab23ab1c009f2965dcda/multidict-6.8.0-cp315-cp315-win_arm64.whl", hash = "sha256:e886ef8c9879105fe4fc99417447b3a5f35d1131412ce839470bd2089fe2043f", size = 47626, upload-time = "2026-09-09T13:57:11.738Z" }, - { url = "https://files.pythonhosted.org/packages/52/f7/aeb947982197e8b4f5c4da3961ee473ea5a050b94a6ff3b88baf64621401/multidict-6.8.0-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:883284137e25318ed9735b742ae46341a864888fae28e8b6314c4f84da080f08", size = 88801, upload-time = "2026-09-09T13:57:13.957Z" }, - { url = "https://files.pythonhosted.org/packages/35/d8/593948c016c3f850e3cd56a4e0144151eb409d2b8690f0c0ce7f7d33dbea/multidict-6.8.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:ca52b9ec80851366197577154c862c4c4c7036ca76ae94cef5cb59c5cfeab944", size = 53376, upload-time = "2026-09-09T13:57:15.94Z" }, - { url = "https://files.pythonhosted.org/packages/58/b9/097a05bca533027c0477b6a90bf927dbbb4b23cc9090bbb37a2e972af8d5/multidict-6.8.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:91fa75d0a693832106d98f66c849f034f21c828d14437f1fb97d3784aab89e84", size = 51629, upload-time = "2026-09-09T13:57:17.685Z" }, - { url = "https://files.pythonhosted.org/packages/fe/07/938ed21967f12380d0b8861645fb65a942f3669e31d5163ed94d23103b61/multidict-6.8.0-cp315-cp315t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:307c1acd812fe897e7fbe10c6758822e8c04be4e7c60a9f54901cdf8b5ab8bc3", size = 261967, upload-time = "2026-09-09T13:57:19.752Z" }, - { url = "https://files.pythonhosted.org/packages/89/e8/e66bf843fd29c01712dde9edeb9f4ad0ffab06ab4ada4b721ad7bc73b3d5/multidict-6.8.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5caf684986a2490628f059a99dd107b566a2d34cf947f8eb8387e0500a1f90c5", size = 265923, upload-time = "2026-09-09T13:57:21.784Z" }, - { url = "https://files.pythonhosted.org/packages/8a/f8/e9be849b225af28a8eee2c6bfea23594a777c753fe97e2ff7e2180c8935a/multidict-6.8.0-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:77745725125d01fd613b6db043362aa7c6bfbfdb23d45dbfc3d92bf58160af62", size = 239380, upload-time = "2026-09-09T13:57:24.3Z" }, - { url = "https://files.pythonhosted.org/packages/ab/67/4dbad08f5081978c591afae9e836ec9ddae90e9e76be6d6ce10757483dc4/multidict-6.8.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8daafaa0b2eb43f76898ced78b1e0fb91b38c4fa50da516c18067f2a2d578c20", size = 271591, upload-time = "2026-09-09T13:57:26.611Z" }, - { url = "https://files.pythonhosted.org/packages/92/3f/e9c97222d7e104e54e556f118ec7d091ab41a0c10c630f2b97e5b43f5404/multidict-6.8.0-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c68e0c0649d17c2d0339e3674e86a4aeba4a7e6b21c1e394cf947a95433b31d0", size = 276091, upload-time = "2026-09-09T13:57:28.997Z" }, - { url = "https://files.pythonhosted.org/packages/26/ca/728e7ce05ac9c0303554e7162e74d91fe49e65bad7dfbb377f783dd32c0a/multidict-6.8.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d8a5ac357ac283490a8d1899b0383355fd1f8634b14ba0d59e4c0dd97db85556", size = 266493, upload-time = "2026-09-09T13:57:31.256Z" }, - { url = "https://files.pythonhosted.org/packages/8f/74/7c658d2769863af16fb7d7c6be50b29659892a06a632858863eee3a31842/multidict-6.8.0-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:46029e6e27a3ec0dc55b53f58df82d10f04c5e111f78248279b530bedad2c30a", size = 245302, upload-time = "2026-09-09T13:57:33.464Z" }, - { url = "https://files.pythonhosted.org/packages/2d/2c/d4350a20a0e8c66a447d694e8713438262665203fe826c3f4e385f052b72/multidict-6.8.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:8d1046b5427dcafe6e8a0e07527dd74f1ee694006160162f53f3a17f15aad3b4", size = 261016, upload-time = "2026-09-09T13:57:35.651Z" }, - { url = "https://files.pythonhosted.org/packages/1f/78/83df999c8beb72a012cfac42f2b833c4a48f8e836fd4407747b355a2430e/multidict-6.8.0-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:f1f4a220db6ed7c8fd16b6d644ffd1f082651693204daf3275e049fadc849e39", size = 255021, upload-time = "2026-09-09T13:57:37.758Z" }, - { url = "https://files.pythonhosted.org/packages/fb/13/f2c0a2dac6d91f74aa124f3e9f07ec497ceae5ed2df2753d249601cd7262/multidict-6.8.0-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:029897732a9c798737457e382bf84e8c64237eff224a90aea2639f4413c45e4e", size = 263066, upload-time = "2026-09-09T13:57:39.897Z" }, - { url = "https://files.pythonhosted.org/packages/59/1d/730008d4639ace731bbb1399e1ac13cbdf506f7d6fb861d75044ffb3994d/multidict-6.8.0-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:29be9fd289e9ab8f480996ea2f686e1654b80242033843cb11691688329423f1", size = 266510, upload-time = "2026-09-09T13:57:42.39Z" }, - { url = "https://files.pythonhosted.org/packages/ab/ca/bec67a5d206dc5748e50c93f6f71deec14305c3657cfe250c3887caf7839/multidict-6.8.0-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:29631224698de1e42abc8fa7658d830e0aed0029785144b5832b695da5adef2f", size = 239423, upload-time = "2026-09-09T13:57:44.304Z" }, - { url = "https://files.pythonhosted.org/packages/9e/db/5f153fe51fbac7d80f3bb8bd6fab8db8b6cd061e7a11371676dfed3712bc/multidict-6.8.0-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:962f18c59a000f30b084ea2e6b8001521bb315efd4e5f10acf9fb36f366b7882", size = 266902, upload-time = "2026-09-09T13:57:46.297Z" }, - { url = "https://files.pythonhosted.org/packages/89/0f/9efca48a351551de4dc0c183f523109dbe87c645a4732d5f1c70b4880dca/multidict-6.8.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:c60e50bc5b07faac92fd3a20fa21cc8cf3e3f7204d2867b206c73293ebc19101", size = 260887, upload-time = "2026-09-09T13:57:48.268Z" }, - { url = "https://files.pythonhosted.org/packages/df/d8/bb879a62e0809448e53f6237e71670066ecf3bbc5896a7a6705b6628d86a/multidict-6.8.0-cp315-cp315t-win32.whl", hash = "sha256:fc5460940f50dff00731b4132366840ba9685286ea88ea104b661899084f3fea", size = 50533, upload-time = "2026-09-09T13:57:50.31Z" }, - { url = "https://files.pythonhosted.org/packages/fe/62/3e5308d8871636e4b9620e4b3acfcf2b5caf79b19d317690ec13f7fc8b57/multidict-6.8.0-cp315-cp315t-win_amd64.whl", hash = "sha256:b367c342327717d644db4c0ddb37ceb655c84822215ea0773a3a36911b74b71d", size = 55572, upload-time = "2026-09-09T13:57:52.301Z" }, - { url = "https://files.pythonhosted.org/packages/b9/cc/d3c10e10ee3bb7a7b4abbb3157306b2ce7e0018c9c2d16b32b468739d2b7/multidict-6.8.0-cp315-cp315t-win_arm64.whl", hash = "sha256:0c1c4debad7337627b86837abdf0237ca3cb3d7e17de7eab0177c263878546d4", size = 50322, upload-time = "2026-09-09T13:57:54.099Z" }, - { url = "https://files.pythonhosted.org/packages/b1/ee/be4e1a4b7a2b27f4fb6936510d4bebcb41b0562c946930ad26916e069cf9/multidict-6.8.0-py3-none-any.whl", hash = "sha256:75daa15ca16d6285eb2e104b2f05ee6f8d9836c68da3ce5c85f615a0450eed0e", size = 16297, upload-time = "2026-09-09T13:57:56.106Z" }, -] - [[package]] name = "mypy-extensions" version = "1.1.0" @@ -2336,40 +1838,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, ] -[[package]] -name = "paho-mqtt" -version = "2.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/39/15/0a6214e76d4d32e7f663b109cf71fb22561c2be0f701d67f93950cd40542/paho_mqtt-2.1.0.tar.gz", hash = "sha256:12d6e7511d4137555a3f6ea167ae846af2c7357b10bc6fa4f7c3968fc1723834", size = 148848, upload-time = "2024-04-29T19:52:55.591Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c4/cb/00451c3cf31790287768bb12c6bec834f5d292eaf3022afc88e14b8afc94/paho_mqtt-2.1.0-py3-none-any.whl", hash = "sha256:6db9ba9b34ed5bc6b6e3812718c7e06e2fd7444540df2455d2c51bd58808feee", size = 67219, upload-time = "2024-04-29T19:52:48.345Z" }, -] - -[[package]] -name = "pamqp" -version = "3.3.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.11'", -] -sdist = { url = "https://files.pythonhosted.org/packages/fb/62/35bbd3d3021e008606cd0a9532db7850c65741bbf69ac8a3a0d8cfeb7934/pamqp-3.3.0.tar.gz", hash = "sha256:40b8795bd4efcf2b0f8821c1de83d12ca16d5760f4507836267fd7a02b06763b", size = 30993, upload-time = "2024-01-12T20:37:25.085Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ac/8d/c1e93296e109a320e508e38118cf7d1fc2a4d1c2ec64de78565b3c445eb5/pamqp-3.3.0-py2.py3-none-any.whl", hash = "sha256:c901a684794157ae39b52cbf700db8c9aae7a470f13528b9d7b4e5f7202f8eb0", size = 33848, upload-time = "2024-01-12T20:37:21.359Z" }, -] - -[[package]] -name = "pamqp" -version = "4.0.1" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.14'", - "python_full_version >= '3.11' and python_full_version < '3.14'", -] -sdist = { url = "https://files.pythonhosted.org/packages/31/4c/33a0ddaaac7bc42f9a542dbaaee8b580ceca3f89bf5da7c498d1fa97ff9a/pamqp-4.0.1.tar.gz", hash = "sha256:9dd13b828e346622793981f14a5df817fce5de998c746209d6c0154eb8403970", size = 137192, upload-time = "2026-07-06T16:37:51.732Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/71/14/1dfc08b743ba995a38dee0ea09beb46a05c7fe8ac53d729095905f7bf11d/pamqp-4.0.1-py3-none-any.whl", hash = "sha256:a547f45128b06e42ce8d7a739b0cfcc40f2c724770622eaaff4a3f587b1cf7d0", size = 32773, upload-time = "2026-07-06T16:37:50.623Z" }, -] - [[package]] name = "pathspec" version = "1.0.4" @@ -2491,151 +1959,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] -[[package]] -name = "propcache" -version = "0.5.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b3/9a/9fbf4e4ec0c2d7f1c32519fff782ef467859b8faa9fbc5331a96f6395d43/propcache-0.5.4.tar.gz", hash = "sha256:ff6b113f50bc066a698db5d944d2c6dc7507168dd3341e255a8892fd0715a558", size = 61545, upload-time = "2026-09-16T00:17:14.386Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/dd/997704118aea215cc5f65c277f5323657b4ba44c1f9a32fb11c8064e4997/propcache-0.5.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b77c313314524ca9c38fbd70f73515d04597ac58c40c939bc0e71eeb4abff680", size = 87225, upload-time = "2026-09-16T00:13:43.864Z" }, - { url = "https://files.pythonhosted.org/packages/3e/6d/ceeca1762ed51230c08d557591404f844ecef5e294550136155572f7faae/propcache-0.5.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8f911c395cef73c510bac566da9507bb6a43e7763d0c79138dc60ee53f11207e", size = 50809, upload-time = "2026-09-16T00:13:45.33Z" }, - { url = "https://files.pythonhosted.org/packages/ff/9a/6be90814d8762952594a9e380161802541bff690f3a2e011dcc28ec193ce/propcache-0.5.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d83b12902eb8bce151259c86c03ba746600b2d994543de46e370cecf96c452f2", size = 52552, upload-time = "2026-09-16T00:13:46.48Z" }, - { url = "https://files.pythonhosted.org/packages/bf/d1/7fd3ffb5ca0e669a8fbf55d9fbb17c50ff5f44becfefcab27a9b9b67908f/propcache-0.5.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9281e922c072158c91974d4589f1dbe0fee6d467f284c28e463f9f5a4d933f4", size = 226804, upload-time = "2026-09-16T00:13:47.739Z" }, - { url = "https://files.pythonhosted.org/packages/74/87/a3e199c45b26587f073db655d19c30d2144b0f883827ba6ab77acf5c7d45/propcache-0.5.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9f3551b8a35c1df3e7ea4d2d86edee15f0dde1bddd434a71744048683544d0ef", size = 234452, upload-time = "2026-09-16T00:13:48.98Z" }, - { url = "https://files.pythonhosted.org/packages/12/7c/08c15c7df256f94a6b4563f74165c3242fb554cdd1909b661d093c31b976/propcache-0.5.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ec6a85f424afa8d23e0d9a094e5dbb6eda01da91c92b9183cd433768247ffc97", size = 240399, upload-time = "2026-09-16T00:13:50.416Z" }, - { url = "https://files.pythonhosted.org/packages/f0/51/5adee15e12a7e314cece4543fbf295b0fe1b504dc78b32dff4c8eb1e29b0/propcache-0.5.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f574e460d1c8a08384a016fdb09ccf3543433263ed6b2f97104f979e64ea57c2", size = 224251, upload-time = "2026-09-16T00:13:51.874Z" }, - { url = "https://files.pythonhosted.org/packages/9d/53/54bd510bb5d473edf66914602885226187218b4e3a5017e9dd80bceea41f/propcache-0.5.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d8e017eeb7482bed34cdb0d61cf2bcfc88d104bbab296a17cd16a6af8aabc70e", size = 202721, upload-time = "2026-09-16T00:13:53.524Z" }, - { url = "https://files.pythonhosted.org/packages/6f/b4/a468700d0526dfb2ca6af5d790125de5894acc7d8ecd99243ecedfa4c4cc/propcache-0.5.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f273dcf7149a50527c4fd1f55cfe9eac0f60753f5af544b4c9352578e20c0874", size = 219148, upload-time = "2026-09-16T00:13:55.068Z" }, - { url = "https://files.pythonhosted.org/packages/6b/d4/bf563e19ac9a5cc47113431337fdf2ee3573859f3f3e0a58e59833e9b75d/propcache-0.5.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:fc2461ecc45f17893f8207e73b46ea8ba93e33630e51cf4af3fbc21d47462b1a", size = 211011, upload-time = "2026-09-16T00:13:56.588Z" }, - { url = "https://files.pythonhosted.org/packages/4b/3d/0189b6537f4a8cd795e685a130a6f47aeda78a5b7b062574705cffc685ca/propcache-0.5.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:279655a16973f1ee2bd2fe79973137681642fd9ae0d89215bba263726eb0dc3a", size = 228148, upload-time = "2026-09-16T00:13:57.913Z" }, - { url = "https://files.pythonhosted.org/packages/ce/b7/59c16e245a549df202b4ebe2de91fa58a67dc4373df9840e372a64224dee/propcache-0.5.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:e9f165403b81fea7e89c932d89046a1e3d9a3a60e8d7ef2f249dccdcb0982bf5", size = 201432, upload-time = "2026-09-16T00:13:59.453Z" }, - { url = "https://files.pythonhosted.org/packages/41/0b/7b19eb20bb0b1f9476d08f6e387ae094dc6870fc06d459ea1f35f28b25d3/propcache-0.5.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:1783582065a1f07f9d9ee1e992e13f15d7dc8fb1eb3a7476d43eb3f2e69d26bb", size = 228923, upload-time = "2026-09-16T00:14:00.731Z" }, - { url = "https://files.pythonhosted.org/packages/3a/f9/b1a0bd47218216b935d019912d6fec1676ae7c07f15fa82bfb54b8d58976/propcache-0.5.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d605bb239b796e82a81c6709548b2bd460ab73b4590cb0c83de8a2dd9694d0f", size = 218327, upload-time = "2026-09-16T00:14:02.091Z" }, - { url = "https://files.pythonhosted.org/packages/55/c0/51e5d1ad504e9529831606534c48e272db751072eee9ff07e4abe37e50bd/propcache-0.5.4-cp310-cp310-win32.whl", hash = "sha256:141fdbd73748db0cf7636035030aaac383d2efde8f34e7bc24594cc776d225b8", size = 43096, upload-time = "2026-09-16T00:14:03.427Z" }, - { url = "https://files.pythonhosted.org/packages/80/57/0a0b4b1122f4ed5bcd1c626d910003cdf5282c2a12f8a1cfa17970b3ded2/propcache-0.5.4-cp310-cp310-win_amd64.whl", hash = "sha256:146f48a9e4812611a7581003b1a39de56c34967046310c4171a68ef908c9a745", size = 46589, upload-time = "2026-09-16T00:14:04.581Z" }, - { url = "https://files.pythonhosted.org/packages/66/87/e71b24adc8ece61782ba3d6f3879e6a07fdb85bce21a9c529524184e3ec6/propcache-0.5.4-cp310-cp310-win_arm64.whl", hash = "sha256:6c7599df2b57ebeea8de011b5f2f7b85de95e76037d43d34b95e328430275487", size = 43982, upload-time = "2026-09-16T00:14:05.728Z" }, - { url = "https://files.pythonhosted.org/packages/1e/40/14b21e505b7921617466576423f188a5c9caddfdaa1cf4b2b8a83d8fe216/propcache-0.5.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:897d1ddf6716e8f47200f7aad9a0efa6cc7586df66c6defa572f9eab379c078e", size = 86393, upload-time = "2026-09-16T00:14:06.9Z" }, - { url = "https://files.pythonhosted.org/packages/e7/4b/5a52e1a7b43563f7d408814194bb23cc8bf214eb6b86639b667a33a8d0d0/propcache-0.5.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9cbfff4423eef4cc6cafc021469641a2b835f610b2647a6c5281903e21b8670d", size = 50431, upload-time = "2026-09-16T00:14:08.025Z" }, - { url = "https://files.pythonhosted.org/packages/05/cf/b5248180bf056cc76acc60c9c6e8c0ebbfdbd1c6cffd31fd14996927b7c8/propcache-0.5.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3fc24f209c1b7f7f688b66b98293954f5504279760999b58920ee12dd8471c1d", size = 52116, upload-time = "2026-09-16T00:14:09.114Z" }, - { url = "https://files.pythonhosted.org/packages/86/a8/7c6cd6bfead1a11f2e411e688640e6d26574cb0bde7dcaa7423b0b65ed7a/propcache-0.5.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:62530ca89187827e4a4fe733f971abe81a7542eeea48ff61995f19b64d7199c8", size = 238729, upload-time = "2026-09-16T00:14:10.357Z" }, - { url = "https://files.pythonhosted.org/packages/5c/b4/442715b2e980df51be52d203549279e027728f24c80b00b5e525e31cd5ea/propcache-0.5.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56fc3f7599528db40b1efa0889a620116e2704144495273d66066e8164e45838", size = 246121, upload-time = "2026-09-16T00:14:11.735Z" }, - { url = "https://files.pythonhosted.org/packages/bc/5d/df0684fc2b1732a01a7bec26d7897369022712422d25b09c37ce7dbc88a2/propcache-0.5.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4f2d880ff60f45898f4acfa152aac8d04e3ee627d90ff4003491bf92239d5757", size = 251735, upload-time = "2026-09-16T00:14:13.053Z" }, - { url = "https://files.pythonhosted.org/packages/c7/06/519a5ebb48b6f94beb48396e55c905f12246a25c3a3608a7ec7bceabf50e/propcache-0.5.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6e9368e87a3efc285e559131092c5db643eb8e56de4ee42064d5baec22ef2bb5", size = 235381, upload-time = "2026-09-16T00:14:14.398Z" }, - { url = "https://files.pythonhosted.org/packages/3c/07/1e0a9bb310830f2245edbd5cd3c6d24a783c053c4efd8e08e386e513c940/propcache-0.5.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:004e685b315646c410771836e72a44f143bbe624f29653a42687815069a303d5", size = 208973, upload-time = "2026-09-16T00:14:15.715Z" }, - { url = "https://files.pythonhosted.org/packages/62/5c/9324fab27d6088eecc47fe4332bf7aaf8c1ded93c36f558391e8a06d41a7/propcache-0.5.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:594eb4c6ec35e7179b058481f4e9f02521b56de16fa577c4b85c76fb1bf8a9f8", size = 233897, upload-time = "2026-09-16T00:14:17.25Z" }, - { url = "https://files.pythonhosted.org/packages/9c/a3/570d92fc952eae93b676f3a1568f4b89264102abd3c982ab6a9ebec58dcf/propcache-0.5.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:2dba2f02d2d5c09ef8a0e6c1a42aeaa451f4be9898cb00b04fe98717da2eb23b", size = 223512, upload-time = "2026-09-16T00:14:18.87Z" }, - { url = "https://files.pythonhosted.org/packages/65/47/26810d889d89bba31db397e6a88f8984af775f5ed6bad0a29dce84324cff/propcache-0.5.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c3ef2818d63bc86071e9d2989ae75a1bc32b8f7059cfd9f5abbbee70c32e2ed6", size = 239043, upload-time = "2026-09-16T00:14:20.366Z" }, - { url = "https://files.pythonhosted.org/packages/89/2d/f9c47691aa024c8299a3afacd78d22a01ab57eb627b481b6089708e71017/propcache-0.5.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:dd2ac8f5b643454c2cc6b6118b13da16e88f4a6434fc3ba61aca384029f04f36", size = 208218, upload-time = "2026-09-16T00:14:21.801Z" }, - { url = "https://files.pythonhosted.org/packages/74/6b/d510c0c378cabbf9d0ac7b663af6d00f2e9074073d93b20c85f24aa5c071/propcache-0.5.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:4054acf80d40456a0537f2913b349718649d8d6458a14ab7f48d0ce28c30869d", size = 240301, upload-time = "2026-09-16T00:14:23.121Z" }, - { url = "https://files.pythonhosted.org/packages/3d/80/c80f6adaaa1e51f0db2dce8c9b3714d94ec45e358a21f9a1910b10b40a80/propcache-0.5.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:40e94adb1e7d39ff28a8bd8d8b8fbd1df6b9f40976dbe379134f1ce058e532dd", size = 230785, upload-time = "2026-09-16T00:14:24.458Z" }, - { url = "https://files.pythonhosted.org/packages/d9/e2/c32a7df3f39caa7f11b2eb37ea5b6960a6946f2bc7c4b8ff97bbdf6d6b6e/propcache-0.5.4-cp311-cp311-win32.whl", hash = "sha256:9f86f7259efe2c951f43e57d471c9b41daa5bfc7db9f67189059cf1ae6d77fd9", size = 42747, upload-time = "2026-09-16T00:14:25.715Z" }, - { url = "https://files.pythonhosted.org/packages/0a/8a/3db6a3543d8101263b4c52978b6276a04ead2caff2c5ab880d934f47bd89/propcache-0.5.4-cp311-cp311-win_amd64.whl", hash = "sha256:e904d4d01f36bd6e197590be1533c44e06058771e0746dd073a8ebb3ef880858", size = 46268, upload-time = "2026-09-16T00:14:26.996Z" }, - { url = "https://files.pythonhosted.org/packages/41/07/5222e2665bbf6e45847492ecbf3b9f3e4975a0ae300e5fd465df7d48ce55/propcache-0.5.4-cp311-cp311-win_arm64.whl", hash = "sha256:d42a9a856a4a6e2f6c10f1318c07e7daa498d6593abe745c71dae4521a26ca39", size = 43547, upload-time = "2026-09-16T00:14:28.143Z" }, - { url = "https://files.pythonhosted.org/packages/71/cd/348d58f142aebc4873345c6b31087629182ca6e0f2b3caeaa528cf882eba/propcache-0.5.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b28f41fa3b8c6900457f858ec5b03998f3a6d535fbc1bb2edec5961ea05ec429", size = 87285, upload-time = "2026-09-16T00:14:29.362Z" }, - { url = "https://files.pythonhosted.org/packages/df/f4/f3ffaee281b276da854ac1d7a6a506d26cbc62ea2e623756f1d0a4a1ba1a/propcache-0.5.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:dcbf346a318a5e30063f547630b02bb787ce2f45b6368d5da143660b6a3835d8", size = 50984, upload-time = "2026-09-16T00:14:30.473Z" }, - { url = "https://files.pythonhosted.org/packages/25/88/1d7df7201750b37765ef2b23bc1c526c028dadde80afa0f57a118fc01182/propcache-0.5.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:87a3caecf8095e48dc72f84bfa42e23a848cf410cc9cc13031fba4869b706a21", size = 52460, upload-time = "2026-09-16T00:14:31.692Z" }, - { url = "https://files.pythonhosted.org/packages/83/4f/48865bd02a16ee5236bc46166b2946f37b93e07b0eae355dac0be0b216ca/propcache-0.5.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60a64cbccaa11b7760ce705a14ada17ba459e7ca9f23ba587eb013821032d7ef", size = 251768, upload-time = "2026-09-16T00:14:32.908Z" }, - { url = "https://files.pythonhosted.org/packages/b0/19/3742a5eed62317b03b4002ee865dc9fd720308bdd0da1f29a5786c630311/propcache-0.5.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a74bfa37147cc08fb29df10bd9c16f40fa7f860cd3a6d2fff853323a94f6e17f", size = 257723, upload-time = "2026-09-16T00:14:34.267Z" }, - { url = "https://files.pythonhosted.org/packages/cb/d5/ee6350fb0be9122bb6c67082a876d34b90d980d100c106af4b81023e04f4/propcache-0.5.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a4d7a54719b67338a305dca2ce6aafe366817df94ddfd4b5514374356f5ca546", size = 265597, upload-time = "2026-09-16T00:14:35.56Z" }, - { url = "https://files.pythonhosted.org/packages/85/9f/83a07b6ec0e043c050cfdd35fb0cf1b7897b91d554d6eea293740309afe7/propcache-0.5.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2814ecd8e818f487bee4b0f921bc4d1c176cc5fc71ac0f072d0fa67eda4ac14b", size = 250424, upload-time = "2026-09-16T00:14:36.894Z" }, - { url = "https://files.pythonhosted.org/packages/33/2c/a763a8251f50fba042af0fb1f02bfec4b31381e40aff760db2be7b2e1f84/propcache-0.5.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6af4693716bfb03f1752ef1b30faa593db2c01d5272e9b8564a1549452a979ab", size = 216748, upload-time = "2026-09-16T00:14:38.369Z" }, - { url = "https://files.pythonhosted.org/packages/6a/e2/4d11bea8fd6a777149c6c20645f873952eab5de3a2497aa11648ec9ab6ab/propcache-0.5.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4fbc1a15dc8cd1689508758d626b372b1f09d28d9577667feaf9e6bfcd8efcbc", size = 246533, upload-time = "2026-09-16T00:14:39.82Z" }, - { url = "https://files.pythonhosted.org/packages/9f/36/6683597de4907e70c717e3588c541202c66086a72ff3db58be49de66e72c/propcache-0.5.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:cdee8205a44d0be91bbac4c41b95d86641b72dfc7aef1279400e4fda3f26a937", size = 238173, upload-time = "2026-09-16T00:14:41.259Z" }, - { url = "https://files.pythonhosted.org/packages/85/84/cb08d79f1762daafeb2b030c470cd0c725c97b8ad67412457c6f35c53e9d/propcache-0.5.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:9a2a8a50a93dee0268a860a07fa3b4bd968f8ce4dbd794957da772f395368526", size = 251128, upload-time = "2026-09-16T00:14:42.652Z" }, - { url = "https://files.pythonhosted.org/packages/c2/0d/41b848036db6621370c1f2e5471a7da8149c730f8552a5257567721f4576/propcache-0.5.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7ffafcbfc7b549ab940047e505c831eabac5e67de53e1bc174adbc5285c55944", size = 214821, upload-time = "2026-09-16T00:14:44.112Z" }, - { url = "https://files.pythonhosted.org/packages/f1/b7/adfae4bf9c63bccf12e2d9690a175c6579047a6eec3b5a6a5f51428c15e2/propcache-0.5.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:d1f5a500bfcbb2c0ab85e98a0dcd70f5899d34efe365a0187700369a79603031", size = 254793, upload-time = "2026-09-16T00:14:45.429Z" }, - { url = "https://files.pythonhosted.org/packages/51/6f/eeca9647245d5f92e87d53e5f14335bb42fce1a7e6842c8045b364eded8b/propcache-0.5.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8a235f73d6e020855dc29dff012d920c02ee0feab8d73a24185a7569f4be1161", size = 247134, upload-time = "2026-09-16T00:14:46.976Z" }, - { url = "https://files.pythonhosted.org/packages/5d/a9/424e38838793d37160b4379c702f61c74c598fc6cd17204adbe3c554f7a8/propcache-0.5.4-cp312-cp312-win32.whl", hash = "sha256:b3083bfe87f95c756e610bd8025f26cbd1cd4aaa03a422f2d65efb7a97cd53d8", size = 43073, upload-time = "2026-09-16T00:14:48.338Z" }, - { url = "https://files.pythonhosted.org/packages/58/7b/6e8ef26f6d510a7916064fec68d55fcbfbdf7eb01e377480d66a122152d8/propcache-0.5.4-cp312-cp312-win_amd64.whl", hash = "sha256:98914de2c4d7f0f9f4a8c6ea4bf05841f4175796941e3ef7d47eb718f22311fb", size = 46190, upload-time = "2026-09-16T00:14:49.99Z" }, - { url = "https://files.pythonhosted.org/packages/08/b9/72028c5b56ced97f456de6aefa79435ca64d7f77af78ea8cf3c76fc5195f/propcache-0.5.4-cp312-cp312-win_arm64.whl", hash = "sha256:8876b39961e33d912afe3c1bee18ee564fdad0206f873cc15d522756b7f50737", size = 43075, upload-time = "2026-09-16T00:14:51.155Z" }, - { url = "https://files.pythonhosted.org/packages/78/4c/3b1365d58a667689e067e13d055fcd92bdf8d9a2fca3d9201b47ed5b3631/propcache-0.5.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:36c0d9db44b523ef93d03341b1c42d69ff01d673c053d1b1c6c3a363bcaa39ba", size = 85290, upload-time = "2026-09-16T00:14:52.342Z" }, - { url = "https://files.pythonhosted.org/packages/8f/61/5f9c29c3aa67c30238c4eadf95149b1d983a48f69b86b0cff927a7d6df13/propcache-0.5.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1d52a05dc417279f7e5c7618c5dfbbc29923aaf9bc0a5c1802ddcebf54c61a0", size = 50027, upload-time = "2026-09-16T00:14:53.67Z" }, - { url = "https://files.pythonhosted.org/packages/25/7d/c1ab1ef09e9d4d835be5d58c0a32a1e1de8397abaa4e502a9d4141328cad/propcache-0.5.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:44149f46500a0a41b95b4d99c2e586a77319539730607b9892974a092788b111", size = 51425, upload-time = "2026-09-16T00:14:54.826Z" }, - { url = "https://files.pythonhosted.org/packages/73/36/0093091ebb270fcd1bc1f6e095f93b2e0ed7f1011c28837dc2dbe5f96b99/propcache-0.5.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dbab5f5ff6897c81f355d079010cdae85b02e5a0b518b5251523b8ad8ae9ac3c", size = 233595, upload-time = "2026-09-16T00:14:56.09Z" }, - { url = "https://files.pythonhosted.org/packages/ae/8f/0de9d4c8e05ce0be71b436919a216bd7fc5cc6e2691c0602295efb22b9ed/propcache-0.5.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c3e98c55bde2bcf7db3c70d1aed7ae9aa8aebbf19a250c66645cde44cdb8b867", size = 240318, upload-time = "2026-09-16T00:14:57.674Z" }, - { url = "https://files.pythonhosted.org/packages/7d/71/2b35e91455209b85ee98f7859583e0814fab57d3af0f2381aaee34c37304/propcache-0.5.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:db3ae52ccc150dbc84704e9d642743897f3e1c54742ff34cacb661e52e3818a9", size = 246649, upload-time = "2026-09-16T00:14:59.352Z" }, - { url = "https://files.pythonhosted.org/packages/ed/74/08e6c1faf26ee2732023a3828787ba535557122774f4a386b1f715cbd8e0/propcache-0.5.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f85915e00dcb1cd9f2f890ead064ed40a27df06f0db65be427b29482ae357572", size = 234316, upload-time = "2026-09-16T00:15:00.696Z" }, - { url = "https://files.pythonhosted.org/packages/5c/9a/08385733c9321c9bb78039d3ff31045e4fca962d9665023c4eb70f998819/propcache-0.5.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c2ba30a89035b57b73e00475de948521602f543d79ce01db10b04b36c4c76fc8", size = 204666, upload-time = "2026-09-16T00:15:02.019Z" }, - { url = "https://files.pythonhosted.org/packages/1d/f4/e87bc7629af9a14a752b218764a78742d73c2c563ac58315da6841f0cbe4/propcache-0.5.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ae58f361bd5dae942717c65d3413b478c70aea9c462599e7b9adad3731db3894", size = 225900, upload-time = "2026-09-16T00:15:03.394Z" }, - { url = "https://files.pythonhosted.org/packages/d9/6d/11014938d3fe9bea2ea2dcf930f26ed565bfb2f5be3c756362ea48c92636/propcache-0.5.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:96f7c5c15656040ddcbc51e56dc59b58aa25999d743c126abd425b9766ab43e9", size = 219988, upload-time = "2026-09-16T00:15:04.811Z" }, - { url = "https://files.pythonhosted.org/packages/dc/72/fbf17c589f92c0b3bbf6709a425661f8ef2ed0d46b38985a7d7b5a0f6b91/propcache-0.5.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7cc528e760a8af06f2b13e9b9f362cd90c7c718ea61228a96dbd31ba16ed7f47", size = 233611, upload-time = "2026-09-16T00:15:06.498Z" }, - { url = "https://files.pythonhosted.org/packages/55/7e/dbd637572a279692e5518d117274a9331bf5faac59f191d30e82521a3ec7/propcache-0.5.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:425f8cc86ab5018b4b8d4a23bc8e74d964bd3d757c3702e301aa79be76c53f6c", size = 204333, upload-time = "2026-09-16T00:15:07.961Z" }, - { url = "https://files.pythonhosted.org/packages/ba/5a/f99c92068f1e0f5c886899ce0e4a619db376ca98c5279d93f95bd86906af/propcache-0.5.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:a5793c7698a53f56f4a1889a4737c7eeb1b7ad0842fa6b1abca22913ff79c8c1", size = 235177, upload-time = "2026-09-16T00:15:09.334Z" }, - { url = "https://files.pythonhosted.org/packages/ee/28/95456fabd2daf6be89049a13fbf03341756014d2959c83d12957d4c49694/propcache-0.5.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c02c0e570c5c7e077b0181a9f3cdb7d4c3617d1cda6b5c95bd5d34022923d82c", size = 228982, upload-time = "2026-09-16T00:15:10.729Z" }, - { url = "https://files.pythonhosted.org/packages/b1/bb/df90f62c9cf7c93ea235f6f9405143bba802914607317266dd81fc8d737e/propcache-0.5.4-cp313-cp313-win32.whl", hash = "sha256:3e413d7a4a9b4866b7a761d6060d434b64d23cd35122eda3b026a0bbe8196b25", size = 42611, upload-time = "2026-09-16T00:15:12.111Z" }, - { url = "https://files.pythonhosted.org/packages/01/bc/e0a7b84af04ec02d73a48aa71f091e1e4a2107e3074b7ce12195b66901f4/propcache-0.5.4-cp313-cp313-win_amd64.whl", hash = "sha256:0c889f6fa84957bc7e8b4eab71fd16a0455068d5045e3aa40c733071d2b2fd77", size = 45342, upload-time = "2026-09-16T00:15:13.519Z" }, - { url = "https://files.pythonhosted.org/packages/9a/70/50b031cafe72a5c1878b903ee87303f71313345566bf3d6ec202e5ddc9ec/propcache-0.5.4-cp313-cp313-win_arm64.whl", hash = "sha256:69fc35c0779522da366c563e5faf203ffc1f8ff0021d5b1337fa4efa5be73177", size = 42408, upload-time = "2026-09-16T00:15:14.788Z" }, - { url = "https://files.pythonhosted.org/packages/33/c9/07e227b930c8ae513b8ef1aae3793499be097bffcdf7aee4fb8b33db4cd1/propcache-0.5.4-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:e6720ba44ad7e72174314d0e1fb0172494cff5c73a3a8a2159c3d2402ff15565", size = 85933, upload-time = "2026-09-16T00:15:16.073Z" }, - { url = "https://files.pythonhosted.org/packages/e4/e1/6710bb44510c4e4a8e0f004bbaf3cecfd048141309c77bae56d4e5a6ebc1/propcache-0.5.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:4cfe0a92ae30151869e67a4b5f5e105e4e03ad30b3f38e5211b5bf77d0881993", size = 50179, upload-time = "2026-09-16T00:15:17.377Z" }, - { url = "https://files.pythonhosted.org/packages/e2/22/b533b493d7025456f44518b33e53e000021a20fe7c27b88cf3d341df7186/propcache-0.5.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d759d05634f1b038fb625a66662a8c85e5a8fec912da381b5149ddac107482b", size = 51942, upload-time = "2026-09-16T00:15:18.589Z" }, - { url = "https://files.pythonhosted.org/packages/f1/74/70ac8430e28f21e442c7bcb964eb46c4363f6881ade4aa0e978bfd8d503a/propcache-0.5.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:251c63dd46a0659bb875cb254dc4c1e79ee91a847c737cd62373295afc2235dc", size = 232647, upload-time = "2026-09-16T00:15:19.905Z" }, - { url = "https://files.pythonhosted.org/packages/72/95/f222f13b6fe623310be0eb61a673bf26df439ce27e563ca8e422d0818777/propcache-0.5.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7a8d5ff04eb1f85698a78d20c62a14676e7b960dcafde09a388d60ad377d355d", size = 241541, upload-time = "2026-09-16T00:15:21.3Z" }, - { url = "https://files.pythonhosted.org/packages/a2/3e/763e370340db16115c5e63ad46e21ef0770a7f06928b3d3b62d8f8edfca4/propcache-0.5.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7b9100a93b372418d8688f3f2a3e5b45c64d70ca4d6176e121aca1e3bfc1e32f", size = 245332, upload-time = "2026-09-16T00:15:22.802Z" }, - { url = "https://files.pythonhosted.org/packages/96/d3/e97cd6f5de2176bd90ed4076c7a9b5e09d0f0b9687d00a576507988bb62c/propcache-0.5.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cc07876cfb079b6f6f36d21ce75784ad6c2c6b563eeac0ed26c2fa2669b85df9", size = 232757, upload-time = "2026-09-16T00:15:24.374Z" }, - { url = "https://files.pythonhosted.org/packages/f9/4c/6766e5f60bcda26d244333aa71d0a702c1c9b21b251d543c7af5953d1eee/propcache-0.5.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0951315a6b3142ee2167404d707743f0157c110091342b1aa0accac5cf0e4acf", size = 204389, upload-time = "2026-09-16T00:15:25.667Z" }, - { url = "https://files.pythonhosted.org/packages/b8/5e/ec4bb09a70b26ea99d76a8292c3383b960b296de2b347ac9986678f1761c/propcache-0.5.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bee7d3aed13d56f54e681df38c3a23031bc9e3863f687d9d598825c9146acd7d", size = 228217, upload-time = "2026-09-16T00:15:27.11Z" }, - { url = "https://files.pythonhosted.org/packages/e1/7d/b53922ba7d9e5bf797324e63aa05906ec240871899f779628df068743e2d/propcache-0.5.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:4e985382be6d15da8d0c2710a6fa7b9070fc9ecdeefb7f580e88373984ec8be3", size = 216947, upload-time = "2026-09-16T00:15:28.532Z" }, - { url = "https://files.pythonhosted.org/packages/ff/39/b62eee45e5ea4de094a258cbb3b01c1e856ca51ddfd95b43135c5effd1eb/propcache-0.5.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:9e9ab13760aa8b6d0881ae7cb04fd891d8d490cd2554ea8e79bb278399169bcc", size = 233457, upload-time = "2026-09-16T00:15:29.977Z" }, - { url = "https://files.pythonhosted.org/packages/cc/a9/feec61ed296d993db9dd097e0f6723e3f576a647722367547495e4c5b05c/propcache-0.5.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:1b2f3bec4261a94019575481c726c29850f72e27907773c75b1de421e20e9f9d", size = 204131, upload-time = "2026-09-16T00:15:31.74Z" }, - { url = "https://files.pythonhosted.org/packages/92/4d/411ef380cddad28dc001f1c6d75ec72c76cd3817030f68ec1ccfba0ec6c1/propcache-0.5.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:720cf832eb2d0b0dfee129cb3335a26f6ce3cc45ee1187e8f0731758caa16792", size = 234820, upload-time = "2026-09-16T00:15:33.087Z" }, - { url = "https://files.pythonhosted.org/packages/15/37/c988229753629ef1cfd5198337a83e624780ea2b3787efe9e747c05aad2d/propcache-0.5.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9fb0a5be8d9aa213150e8d8148a42aca4984b285bcad1e69587dc4298edd929b", size = 228350, upload-time = "2026-09-16T00:15:34.533Z" }, - { url = "https://files.pythonhosted.org/packages/12/49/5ef1c5cf98591da3c5b952b39e6a298084cc1ce353bc70f85e82397a5036/propcache-0.5.4-cp314-cp314-win32.whl", hash = "sha256:30cc1cebaf9aef49db06357a50398323ae04d70460c0491837d026ab7d6452ea", size = 43578, upload-time = "2026-09-16T00:15:35.957Z" }, - { url = "https://files.pythonhosted.org/packages/1e/9e/a0ac821a2229186af5e2e3c3635a78abb23cfddca57f38513ab5d70420f3/propcache-0.5.4-cp314-cp314-win_amd64.whl", hash = "sha256:0a095db8e15a6020db149ecbed6461939fe74f6acaa3ae8b702a1fe8c38cd983", size = 46304, upload-time = "2026-09-16T00:15:37.655Z" }, - { url = "https://files.pythonhosted.org/packages/a1/19/c8d0d36a9d16cba5dcee67d389c9333b988c8986a653a61c00a451817a46/propcache-0.5.4-cp314-cp314-win_arm64.whl", hash = "sha256:45488d1a5f9ab5bd90aaa1ca20f50fe1922b8ffad71a2009d2adf41355897aac", size = 43440, upload-time = "2026-09-16T00:15:39.091Z" }, - { url = "https://files.pythonhosted.org/packages/2c/e9/42f1da77cacfc184e6ec929557ef653b7961bbf6f1da460b9221273948b3/propcache-0.5.4-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:53eaa697c4d0422ff4cb714d00231b43352064d97b944033b30c1d57cc506ec0", size = 90672, upload-time = "2026-09-16T00:15:40.306Z" }, - { url = "https://files.pythonhosted.org/packages/cf/2f/4b79940908c6ab8c795097c102999d7bc1f7e0b8604dfd1c232f9d99d67a/propcache-0.5.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:886b59c4d28ca97dd23b025fdfc50a0356be934efbbbca89ad26230067f86fe5", size = 52586, upload-time = "2026-09-16T00:15:41.575Z" }, - { url = "https://files.pythonhosted.org/packages/eb/07/02196ae6320c110235bb343f90dbd34be41f8b8964a3ee30db84ec12579e/propcache-0.5.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3fa15757fea1dfcd5b7745cad9f4638929605531bd4018ab2adff7955f1a403d", size = 54335, upload-time = "2026-09-16T00:15:43.027Z" }, - { url = "https://files.pythonhosted.org/packages/6f/44/f48b9a131985659924df5fa5093f68fe72c7ee375329802989ba3126efc6/propcache-0.5.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6f0093ac3e9daada202c2082439d414a625c57184727a46e112a3fb2a81cb788", size = 297567, upload-time = "2026-09-16T00:15:44.373Z" }, - { url = "https://files.pythonhosted.org/packages/04/a1/418d956d2735139f77fc35262179f1f52c23aa666de5a8ab3819c1ae7854/propcache-0.5.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3cd3a7edb6b95b9b33998135ebfa18d709da82290fb8f27c858970b5a12c8b56", size = 297477, upload-time = "2026-09-16T00:15:46.048Z" }, - { url = "https://files.pythonhosted.org/packages/69/fd/ff811fdb6d3d3e67fd9bbfb75881675d34a42d0ef29a45d33e3e233dde07/propcache-0.5.4-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c174bfd1c48a1b51a3078e95586dde718374bac79719ab3541ec9e74aec40574", size = 302669, upload-time = "2026-09-16T00:15:47.458Z" }, - { url = "https://files.pythonhosted.org/packages/fc/57/527910c455b5ec62f6871bef45d4f79fea16cb8c966ba0d4a07f0339ddc4/propcache-0.5.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a219f0ac59817a9114dd2aa57c13180f993e819ba658c7ddab4b66ed1ee0d370", size = 287908, upload-time = "2026-09-16T00:15:48.99Z" }, - { url = "https://files.pythonhosted.org/packages/1d/86/f69ab82707534a0cb2057bdca04f9200a71214c7551800f9d34d6ac39e4f/propcache-0.5.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:17a7400cec0256f0a71ae71f9da398f9894c956ff6668a1c9d317b3367316320", size = 249804, upload-time = "2026-09-16T00:15:50.486Z" }, - { url = "https://files.pythonhosted.org/packages/27/19/60677af50d93be4256213de7cd487f056944c048b9c0b6f2e45b3a30f666/propcache-0.5.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:978f28401afbc76cdc3df9e1717b4229a06b626a1dcc75db4e1f2beb3884c3e9", size = 282344, upload-time = "2026-09-16T00:15:52.029Z" }, - { url = "https://files.pythonhosted.org/packages/7c/f7/a0057808a91fb3b6a5f3602b528f0cdcb3d53e0ff8315d73fabdfdf8fec4/propcache-0.5.4-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:4a1f4f5ffa55dce6307631f3cb2948e117e665966ea512e0d502b16c24f567e7", size = 270167, upload-time = "2026-09-16T00:15:53.466Z" }, - { url = "https://files.pythonhosted.org/packages/83/c8/f4a865490df0dc0c8531d4e59ac411cb6dc24bb255d2396a6f1c60a368f4/propcache-0.5.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:213bb68d9ced5cf2bf717b1071bf2b09b4b04c426256f9fe6d054c60318424c4", size = 286551, upload-time = "2026-09-16T00:15:54.995Z" }, - { url = "https://files.pythonhosted.org/packages/b0/67/b4faebde9da4e8173d0e5a30e8cd31335914af7ef350b988f27fec588cfd/propcache-0.5.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:286867fb156488c251a3721766e380ac4495e4fd6b51aaa1403d89ce7f4359d9", size = 249595, upload-time = "2026-09-16T00:15:56.505Z" }, - { url = "https://files.pythonhosted.org/packages/f6/40/52e1dd5636e9f5a27f6b5a4b4e2f33c322fd72afe956c397d82523ec4a80/propcache-0.5.4-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:445ee3bfb46e85838387fb3c536a73cc0b994dc192b004e40e170adc54aa2a7e", size = 286700, upload-time = "2026-09-16T00:15:57.985Z" }, - { url = "https://files.pythonhosted.org/packages/d5/0e/30b2b324b93ff31a0bab539c102aae59e84e444031b2742150a7646aa1bb/propcache-0.5.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:48cb48c5346a97de792254af77715aa2529c2a1ebc5f586aa0aae44a02f1fe57", size = 280500, upload-time = "2026-09-16T00:15:59.487Z" }, - { url = "https://files.pythonhosted.org/packages/64/36/721bb59f682ff060d0c8df64274fca8cd0521b1a54506c2eedaef795b7f5/propcache-0.5.4-cp314-cp314t-win32.whl", hash = "sha256:03b229037d25b801e7af53fd52b9fc49d9439b036fca1e087e02780631adfa97", size = 46121, upload-time = "2026-09-16T00:16:01.349Z" }, - { url = "https://files.pythonhosted.org/packages/c1/86/0b1b80fa1ac3a0aac44e2922a6964fbe9cd52af5eab8fa933bf9e90b030c/propcache-0.5.4-cp314-cp314t-win_amd64.whl", hash = "sha256:8a1fc236528c457cd739c88abe823da851b7ab645d72792f88658114cc340c12", size = 49154, upload-time = "2026-09-16T00:16:02.901Z" }, - { url = "https://files.pythonhosted.org/packages/69/4f/9fe6f05a47cb550c823155052116f710064b6be5c6e8ec4e9faae7e18115/propcache-0.5.4-cp314-cp314t-win_arm64.whl", hash = "sha256:135036c5cfc93864affb0f9af9a27e5d7a71cb7bd745e7b6dbfc2d56cc30e827", size = 46005, upload-time = "2026-09-16T00:16:04.266Z" }, - { url = "https://files.pythonhosted.org/packages/58/25/895a11d1e4c5c2acc6d816e2bece34e02d9dc92f2182ae276cd819e9e804/propcache-0.5.4-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:45bf2e730ab8905d0527fe05a86500f406e64305c34cc81ebe64b4617cab9760", size = 85634, upload-time = "2026-09-16T00:16:05.599Z" }, - { url = "https://files.pythonhosted.org/packages/58/41/c0acd69271de7a1cf439e77d5d60c18575fd09bad56e798b95fa23458ea4/propcache-0.5.4-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:31eb43ba2edc704ab2ec27815315dd8a19def0fb16215be4cfe8d32fe78ffd51", size = 50084, upload-time = "2026-09-16T00:16:07.384Z" }, - { url = "https://files.pythonhosted.org/packages/a5/1a/ad561f99f90884089e6403b76c220610809429ba868a81a2e7ce115d32e0/propcache-0.5.4-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:174507f82d3594622acb1dd2dafecf2d899d6d506335494e7107767bf05f3aae", size = 51692, upload-time = "2026-09-16T00:16:08.956Z" }, - { url = "https://files.pythonhosted.org/packages/e9/07/057bdd3a9609ffad59b06239cceee784b047f6c720247bfaa36d2103e138/propcache-0.5.4-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:50e337653721d20ead710da33bf44487fbe8a0db8782714b60306481e9f95b51", size = 232947, upload-time = "2026-09-16T00:16:10.466Z" }, - { url = "https://files.pythonhosted.org/packages/fa/dd/d36ad35986718530498a65e45e3713f9f0e6a580f192ef02d2ef7cae9b52/propcache-0.5.4-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0d21d0d2c82bbfeb1677a9711f38df968f9837576102bb4add1bd449d28d88f1", size = 241250, upload-time = "2026-09-16T00:16:12.056Z" }, - { url = "https://files.pythonhosted.org/packages/fb/81/f1459415cdb6c10d46942779de39bb59a77b38e5a76bb1def9227962eb45/propcache-0.5.4-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ccf4f7a79e26bb7efb06ecd50c177833b71df05cbc748701372325e6bcc17f6f", size = 245150, upload-time = "2026-09-16T00:16:13.596Z" }, - { url = "https://files.pythonhosted.org/packages/ce/4e/58b9b1460afc97a4c0b17ee89af701c4011d4d7f46470eba3aaff76a8069/propcache-0.5.4-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23278f808cd81d5ada7184a76606b925fb3389c60e1077b2cd7da7b1fcf0553c", size = 232166, upload-time = "2026-09-16T00:16:15.126Z" }, - { url = "https://files.pythonhosted.org/packages/b9/c6/5a79e0eda3e7b6987d03d8c622ff6d52a42165a12e8418eb37694b9cc4b4/propcache-0.5.4-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e738ab81179510ce79b2eac9a6ecf47feffd9e76d1c72e403005dddb6e36c06c", size = 206085, upload-time = "2026-09-16T00:16:16.713Z" }, - { url = "https://files.pythonhosted.org/packages/4e/72/940aed42c73f9da345ca2de0f6e835c726498159abca5f1ef14fb0a2af8a/propcache-0.5.4-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:a419ee85e654927baabda3929c03c0cc1112bf472ff0dfd6142f4e3a81ca4162", size = 228460, upload-time = "2026-09-16T00:16:18.352Z" }, - { url = "https://files.pythonhosted.org/packages/85/71/3f54e1535c8f323d91ba566044d7c2b39ff6f6a2f1d0bd9071779d07b9b3/propcache-0.5.4-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:b61805357d966680acf68b3b6d49772631ed9df44ebece10ff1460e117a7da8a", size = 218350, upload-time = "2026-09-16T00:16:20.064Z" }, - { url = "https://files.pythonhosted.org/packages/a8/f4/025890cc389ac3ec485ecec607d4a7ca47e15bfa2a465746ab98af602536/propcache-0.5.4-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:58134228927cee6c047d626c08e60a81be604a20578a12ce752cc5c9a84d4826", size = 233156, upload-time = "2026-09-16T00:16:21.624Z" }, - { url = "https://files.pythonhosted.org/packages/04/29/b39cae08c87c140d3d274f0a2c058cb5588e836175c3309e260b230ab07d/propcache-0.5.4-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:350b272b2279f4135a64fc0c304a5d08e28a137c9573442c606152446638a831", size = 206206, upload-time = "2026-09-16T00:16:23.204Z" }, - { url = "https://files.pythonhosted.org/packages/18/61/e16462ef18a87247dc9ebbd5c606f46d5ce67e708bd9cc734dd0d9222564/propcache-0.5.4-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:45bebbe252550fec975ba3b62bc6f931643cfd3b5464ef47619cf3fef154e01c", size = 234469, upload-time = "2026-09-16T00:16:24.841Z" }, - { url = "https://files.pythonhosted.org/packages/9f/84/b6a1490922427204fc47df920ed002eec709621de6b79b11592bf45c623a/propcache-0.5.4-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:ada748108a43d29b7c328ba7db3755327cd94f028bcc1a7ee3f0addcfacd9c38", size = 227311, upload-time = "2026-09-16T00:16:26.549Z" }, - { url = "https://files.pythonhosted.org/packages/ff/5c/5a59527582e9bcb694b2f08b9894134b65a0f5f79dbff174f054f5f74ed0/propcache-0.5.4-cp315-cp315-win32.whl", hash = "sha256:ee19113bce2f3acd46432050688b70f61acd6857d75abb9ec96341b7e9ced123", size = 43512, upload-time = "2026-09-16T00:16:28.313Z" }, - { url = "https://files.pythonhosted.org/packages/26/07/93cf699ed363681e754d7c3fad587fb09ef6b65618ee193332ad16a68d7b/propcache-0.5.4-cp315-cp315-win_amd64.whl", hash = "sha256:ceb3e879afac028f93d272c957814695dc5569e4904262dbee92f6c41bd5e4a3", size = 46264, upload-time = "2026-09-16T00:16:29.751Z" }, - { url = "https://files.pythonhosted.org/packages/65/10/fef04fbdcd44a4a163cb5ff5674599c6d6fdefd64a5a459438f9ad2ba042/propcache-0.5.4-cp315-cp315-win_arm64.whl", hash = "sha256:c83acbce9f2b5e3f5f5eda9e53d2001fed22fcdfef81274a9e02d8fd53b70a30", size = 43395, upload-time = "2026-09-16T00:16:31.5Z" }, - { url = "https://files.pythonhosted.org/packages/70/f6/7e2f4dab0b92ab46111bd48cee9ee1e5f519514c44e3779ede5358d7ada0/propcache-0.5.4-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:a5e8ef588c109725dc713ba69aadcac00a1ef90c2ce9c0a8c7075128f569f47f", size = 89825, upload-time = "2026-09-16T00:16:43.115Z" }, - { url = "https://files.pythonhosted.org/packages/9f/8b/dfeff925cb6ced97ede701d5c6a99998da963c6f2e06abbf879c9dac5b54/propcache-0.5.4-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:4d86476a935c88963d9b8e1a9a0d38188790e9622169bfbafa173046846709d3", size = 52159, upload-time = "2026-09-16T00:16:44.754Z" }, - { url = "https://files.pythonhosted.org/packages/24/6c/924c810be5b7cf218ef47e707cf06d34adb4e3f3a31e3c24c55c6d945a88/propcache-0.5.4-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:f5470694918830da62fac9e69133b53d23b736d7070e587b27a4a2be37e08e68", size = 53956, upload-time = "2026-09-16T00:16:46.762Z" }, - { url = "https://files.pythonhosted.org/packages/3f/b6/9ed0a5c939b58b6bed740a05b5d0f919f0b318d03284b4b6d81a0fe8a29a/propcache-0.5.4-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10ef33a68a61ce317e095fd2e202a592ea92392b90944a78c993f0d9a73ab06c", size = 295235, upload-time = "2026-09-16T00:16:48.577Z" }, - { url = "https://files.pythonhosted.org/packages/5a/eb/5ce886e902a2e781dddf110993d5329458a9b1a8626b876c65e5e25bf413/propcache-0.5.4-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5cacf3c9efd09df409dc33654dd077e1c245ba8fb747b0f0236ef41b7c49b589", size = 294463, upload-time = "2026-09-16T00:16:50.539Z" }, - { url = "https://files.pythonhosted.org/packages/f2/88/c98f49183ecd3e5b204a556f0ca47baa02c2206a500fe8c7ec1726297b0a/propcache-0.5.4-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:770e8209d018175fc0063936fa9583b6d27e88c5ad31543f3383d66080efdd62", size = 300081, upload-time = "2026-09-16T00:16:52.423Z" }, - { url = "https://files.pythonhosted.org/packages/27/0d/c5090f9e6f67cbc30a2b744c7bb0f8006dcba5ec1b0d82f866ae1cc7c5c4/propcache-0.5.4-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03969626faf0783a592dfa17e28eac06018bd0b44dafae6943d53b92421a7f72", size = 285360, upload-time = "2026-09-16T00:16:54.141Z" }, - { url = "https://files.pythonhosted.org/packages/ac/9c/34a55396910583ed07926669ab309dde2213a2dec05a7e946bb90ad66908/propcache-0.5.4-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ef3b928d9c984322b5c44e6964d8dbc653da87d2d8ee1647fa6da43072e650a9", size = 248014, upload-time = "2026-09-16T00:16:56.062Z" }, - { url = "https://files.pythonhosted.org/packages/cd/b5/c0a142b656093ca397039dd3fe166cbb87c945712b534546514a24cd2611/propcache-0.5.4-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:7177c43eddf10a0893c4fec52ebb408fdcd7f7d63962caace9180d8f81b14ece", size = 280662, upload-time = "2026-09-16T00:16:58.044Z" }, - { url = "https://files.pythonhosted.org/packages/54/28/fab2809c2e337fe26becea9648e84d5cef46075c91b826acb13e4f9dd04e/propcache-0.5.4-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:420162a77f94eb1cf5ef7893f500016dabd548e73de956785a1dd899cc73006a", size = 266149, upload-time = "2026-09-16T00:16:59.702Z" }, - { url = "https://files.pythonhosted.org/packages/3a/11/7ddf336288b2678a5f054f8da2e2bd1a719f5d4b7de714d9c6bd588a2313/propcache-0.5.4-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:3eb2e820e8e2101407da93f17c57cbb7d225461955fc60105daaba14cd421ee2", size = 283097, upload-time = "2026-09-16T00:17:01.459Z" }, - { url = "https://files.pythonhosted.org/packages/1a/ae/351b1a5225f5473c411d9a612a229ae147cf0cf65c72ad838b87219ea8e8/propcache-0.5.4-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:13e52b6e0bde97dee98ab66552dbff2931649c96f1ac432eac299fe689ec373b", size = 248160, upload-time = "2026-09-16T00:17:03.298Z" }, - { url = "https://files.pythonhosted.org/packages/3f/d0/7f79f061e30d135bb615c9782c94a74652033d00b49254edbbf35a9165a8/propcache-0.5.4-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:12682126712ddc19b70ff819debbd279e58adf1f0c8f8f8138c18ade2044b284", size = 283036, upload-time = "2026-09-16T00:17:05.238Z" }, - { url = "https://files.pythonhosted.org/packages/53/3c/016f1cad8bf4c428d748cf399b2bac603026fbfd6966e6a5579b5c5b6956/propcache-0.5.4-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:3af0c8642b2da4815d86e631232ac8286e17644fad907c19508aa8e7cb4ba8ad", size = 279350, upload-time = "2026-09-16T00:17:06.881Z" }, - { url = "https://files.pythonhosted.org/packages/ea/60/d8f72cb24b412487ed4c397f539117d3b74c3c33dd32020e91fe00a958a8/propcache-0.5.4-cp315-cp315t-win32.whl", hash = "sha256:1df8d8561b21465c5dd56110a01caf897e026d065b4b84e98a488209094272ec", size = 45874, upload-time = "2026-09-16T00:17:08.567Z" }, - { url = "https://files.pythonhosted.org/packages/d4/ef/8bae0a316d406644450522f2f3d44a4e19632f5f3bb60d1d0e6c53842616/propcache-0.5.4-cp315-cp315t-win_amd64.whl", hash = "sha256:02c0a34f16889cf800f10f0247a564d8ce6eeab6ffcd7c87198f769067eb8432", size = 48574, upload-time = "2026-09-16T00:17:10.077Z" }, - { url = "https://files.pythonhosted.org/packages/57/be/bcc053f66a97355683884b448198e79580fae8e8fa4d96b9bb01614e9913/propcache-0.5.4-cp315-cp315t-win_arm64.whl", hash = "sha256:dc4242ca653c9b30ab51c5f8193323e7bc0928f897ee9103201e59a43abcb72e", size = 45625, upload-time = "2026-09-16T00:17:11.377Z" }, - { url = "https://files.pythonhosted.org/packages/f5/cd/785c64ed382f3f04201870267b02783f63b4678c2acfddc177a3ebcc2727/propcache-0.5.4-py3-none-any.whl", hash = "sha256:62c60aec739ed00124573cce1178138fd690c7676352d67a37328c1cf51d7468", size = 16338, upload-time = "2026-09-16T00:17:13.106Z" }, -] - [[package]] name = "protobuf" version = "6.33.6" @@ -3279,15 +2602,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/28/7e/61c42657f6e4614a4258f1c3b0c5b93adc4d1f8575f5229d1906b483099b/ruff-0.12.12-py3-none-win_arm64.whl", hash = "sha256:2a8199cab4ce4d72d158319b63370abf60991495fb733db96cd923a34c52d093", size = 12256762, upload-time = "2025-09-04T16:50:15.737Z" }, ] -[[package]] -name = "setuptools" -version = "84.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6d/44/f5da03a8ef95d369145c5bb53050e7877c9f3d312e128605fd9504829143/setuptools-84.0.0.tar.gz", hash = "sha256:f4695c21257f0d9b537ec2692c941d02ee143b7cc1276941349a546573b2ef73", size = 1168449, upload-time = "2026-08-08T18:27:58.365Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/95/9c/c510029fc6ef33a6275cd2c5d3cecd6613dfd6aa401d57c54f1c18852ccf/setuptools-84.0.0-py3-none-any.whl", hash = "sha256:51a52592b3b99e102b609654876bd65f19f999935166d1352678931132b0c670", size = 818216, upload-time = "2026-08-08T18:27:56.719Z" }, -] - [[package]] name = "shellingham" version = "1.5.4" @@ -3465,15 +2779,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/93/72/6b3e70d32e89a5cbb6a4513726c1ae8762165b027af569289e19ec08edd8/typer-0.17.4-py3-none-any.whl", hash = "sha256:015534a6edaa450e7007eba705d5c18c3349dcea50a6ad79a5ed530967575824", size = 46643, upload-time = "2025-09-05T18:14:39.166Z" }, ] -[[package]] -name = "types-protobuf" -version = "7.35.1.20260906" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e1/6c/e3e5b3e10bc328126a39637c138f9ebfd734bf14342b9f3540039b4ab995/types_protobuf-7.35.1.20260906.tar.gz", hash = "sha256:efd1a3862d4c967dad5512ef8d56b1530ac84f182c41735b94004756518c4998", size = 69895, upload-time = "2026-09-06T06:35:28.308Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/44/4e/f63e826c68f77ef875506d72f225918800346545ee99847bc28f3394f18d/types_protobuf-7.35.1.20260906-py3-none-any.whl", hash = "sha256:5155e48569e0dabff303fdf578db96cd31ea9a4a63b18018a4ceac6b0ae17462", size = 86419, upload-time = "2026-09-06T06:35:27.247Z" }, -] - [[package]] name = "typing-extensions" version = "4.15.0" @@ -3619,156 +2924,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1f/f6/a933bd70f98e9cf3e08167fc5cd7aaaca49147e48411c0bd5ae701bb2194/wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22", size = 23591, upload-time = "2025-08-12T05:53:20.674Z" }, ] -[[package]] -name = "yarl" -version = "1.25.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "idna" }, - { name = "multidict" }, - { name = "propcache" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/75/16/e8be8e2fb175bbf41a0680381a319f1199fae256588241a2ac8677eafb49/yarl-1.25.1.tar.gz", hash = "sha256:03dd38de09bc213e9a8b29761eec33ee1d5318dac0e49d8af36e4d27830e23a7", size = 246245, upload-time = "2026-09-15T19:35:02.264Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/85/92/55fa9ee84cb8ec9930a910b5753936a926f918d8cc8965bdac571479c095/yarl-1.25.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:142c06c4d6a35ee3ec5da08499805e879cb3ca7c1fbfbecb0140fe72403818d6", size = 144695, upload-time = "2026-09-15T19:29:53.114Z" }, - { url = "https://files.pythonhosted.org/packages/16/b4/9edc8e605b16b2eb5bd0c2ccc73e2b15aab583862460767fb43f91f11839/yarl-1.25.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:24ce942011a61953e7d313438038f4d32ff21387b775f58a957f7a07dd55ef95", size = 104332, upload-time = "2026-09-15T19:29:55.443Z" }, - { url = "https://files.pythonhosted.org/packages/ae/de/4204e6646e278b1cd4a9cf73ebe8b7cfcf16693f1249b324eb273d67602a/yarl-1.25.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e23c82b63cd7652fc24d33ed6cc17099d607aa3b4fc4ddc75e95062f3d82df4", size = 104449, upload-time = "2026-09-15T19:29:57.239Z" }, - { url = "https://files.pythonhosted.org/packages/1d/fc/21d74789198ad68a23a209ab0d73fe5a52d39ebbd9945041e72827acdd77/yarl-1.25.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8ee202350cf57abf0e9502a41601841019c25d3db7ff52d980aaf31446254059", size = 116828, upload-time = "2026-09-15T19:29:59.031Z" }, - { url = "https://files.pythonhosted.org/packages/8f/31/90891ddb61848c067ebd1fb505ab5902a6e7d4707c2f3823f67ff37883d3/yarl-1.25.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5df89f769cc8ff94c3d7e7603386fba309d25ce5240132d26c15baa8d0e96c4c", size = 107150, upload-time = "2026-09-15T19:30:00.868Z" }, - { url = "https://files.pythonhosted.org/packages/a1/15/f3d18743d3688b5f01aa83034b6c00665692ab20e1d0c89a29000d4979ea/yarl-1.25.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:83e9f4a25085bd4b7214701a0794ff1f50fc633ffb8bdfebf07abdd81c2db126", size = 124704, upload-time = "2026-09-15T19:30:03.128Z" }, - { url = "https://files.pythonhosted.org/packages/99/37/718789d8004775d6a2db86b2afc72b1d156e0590b88a1b8634ddd4bdb8ee/yarl-1.25.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e636b64d24fd9c38053c5e389a1174c66361fa49dcfd220f4dd35b4abde7cb89", size = 129246, upload-time = "2026-09-15T19:30:05.014Z" }, - { url = "https://files.pythonhosted.org/packages/28/b9/7818ac6dec7fbcd16be2d19495807c01e2c38834e57d41c63356a75e786b/yarl-1.25.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e5637ca8d0bd7fb72648a6c7934af4baaccb697657f7438c9d264fc2abb8b0b1", size = 118071, upload-time = "2026-09-15T19:30:06.977Z" }, - { url = "https://files.pythonhosted.org/packages/b2/e3/8d098cafb30a64df7283b5a5dd0d32d22a71b18a1b9ec071d0776ffe5c2a/yarl-1.25.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:683e362b8ba453080f7489c66f4ea794e751c35b72e7eab3575ef784c2fbc7fb", size = 116180, upload-time = "2026-09-15T19:30:08.836Z" }, - { url = "https://files.pythonhosted.org/packages/20/9c/fbe9432478d87e00eef80242e15179639df162cb3d5d8d978fc56dcb6c51/yarl-1.25.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:df23df54b5114a17c2d0ef192433e2e5a9f0c5178c32375e90b7cfc965f349d0", size = 116578, upload-time = "2026-09-15T19:30:10.782Z" }, - { url = "https://files.pythonhosted.org/packages/b1/8e/b3dfd03732236b86b0bcf06a72a37de412ff6f82e719a947c3c651d4dece/yarl-1.25.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:f53dcd26694f148f738edc052b5a69234833e739f10f4c3287bdfd8ec0f7b326", size = 108886, upload-time = "2026-09-15T19:30:12.951Z" }, - { url = "https://files.pythonhosted.org/packages/83/e1/94dacb650d4963d5f844bc3df7ba70288cb7b68ed5a4d070da71acb19b14/yarl-1.25.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:b8075fe90bc08e40b8b8a1874fab42ee4c7b56af05c5886e9cc841397f916908", size = 124098, upload-time = "2026-09-15T19:30:14.963Z" }, - { url = "https://files.pythonhosted.org/packages/d6/3f/c93f76a218258c7bfc60dd27fd56e9102bdbc625517f4a6ee673295fe4e4/yarl-1.25.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:a8c2b841478068440d8b733005d13a5ef535b9928cbc05f17182d410f32ba449", size = 115768, upload-time = "2026-09-15T19:30:16.736Z" }, - { url = "https://files.pythonhosted.org/packages/a8/79/4d93f13c3b05cda3c962805dec28cbc255c50239b3457808abc5633a00c2/yarl-1.25.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:ca32926d7d77bcc8838425c4c95e040a3ace1cb7dfdae599013458dcda2607ca", size = 122461, upload-time = "2026-09-15T19:30:18.901Z" }, - { url = "https://files.pythonhosted.org/packages/c5/8e/e2ba83b3a9bfc1d3b882ad35fa8abc04a7af4d9356f2e329a23d0c8d889d/yarl-1.25.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:192a866877a49993949ef1975864ad8728bea28ee810f6abe1a0729c2b500426", size = 118295, upload-time = "2026-09-15T19:30:21.07Z" }, - { url = "https://files.pythonhosted.org/packages/42/67/cb5ea1baa0c0ac60bda44ce59f601133154af2a2e67722d3517f8793855d/yarl-1.25.1-cp310-cp310-win_amd64.whl", hash = "sha256:3f4d48a6112712973e676bd792121fee470e432d749177162d9949d5c9460a1b", size = 102929, upload-time = "2026-09-15T19:30:23.122Z" }, - { url = "https://files.pythonhosted.org/packages/2f/85/9a1e98de10fc0e738d517efd2a9984c3e7db6cf35fa72b2b373988a9e9b7/yarl-1.25.1-cp310-cp310-win_arm64.whl", hash = "sha256:48796ea00a303961507dc6c8437c4b325a6fc3f95f7c36c71b91ea9a8150963c", size = 98854, upload-time = "2026-09-15T19:30:25.102Z" }, - { url = "https://files.pythonhosted.org/packages/83/b3/2cea721d495ca57f8f414aa4867ee263f486b274e07463158cf52f02ba7f/yarl-1.25.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9d693bf4bf534e9ba3ae2780cfd577f5135629f7b5ac653490859d0b77864865", size = 143794, upload-time = "2026-09-15T19:30:26.946Z" }, - { url = "https://files.pythonhosted.org/packages/55/e6/cd145cff8e5cf60b8b3c41fbecfa2a45028a8dec3fbc52bec03595ff3d3b/yarl-1.25.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ab2054c5531af2a9ba7b69b8ec91e4f884420e83a8c5e579b013084cb57e5e5d", size = 103993, upload-time = "2026-09-15T19:30:28.661Z" }, - { url = "https://files.pythonhosted.org/packages/ce/7c/fbb40fe2d53747c40aa36a9e2bd2178a202f942bda0b670f3306d4aefbce/yarl-1.25.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:564fdc7085d2245ab84f88882fdb1d6ac0723124bff6ded35bfb1c00f812630d", size = 104010, upload-time = "2026-09-15T19:30:31.069Z" }, - { url = "https://files.pythonhosted.org/packages/f0/0b/5a516f70641092283f57cf3670bdb75e7327bcc0dcb5038697e4dfbfd569/yarl-1.25.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:acae6b45d1ace09b6ba3876da43b88366ef368f73b988c7f57e14231753d4420", size = 116444, upload-time = "2026-09-15T19:30:32.894Z" }, - { url = "https://files.pythonhosted.org/packages/b6/8a/d1a627f827b0a404ae0f5647cab0534959081cde13b0756a783469fb3b5e/yarl-1.25.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1fb2a01ba8cd9c5d2c5dc1ec35e0fc951d04b4f037541d4ac090c993ce58b3d7", size = 107565, upload-time = "2026-09-15T19:30:35.188Z" }, - { url = "https://files.pythonhosted.org/packages/aa/cf/c8e0aaec886840a6c4480fe44eaec7cd4319f79a563b79321473be79c56f/yarl-1.25.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e92b6bcc741b86d67606c40d3cb9c7cc8e6c737f81e31f4a94efc204456c92e3", size = 125006, upload-time = "2026-09-15T19:30:37.1Z" }, - { url = "https://files.pythonhosted.org/packages/50/26/0cce366d54a93cdc8342965dc7663385e4db161625cc1e8b18e786753d24/yarl-1.25.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:72c34ac7ad4314c19362d5ce27626dcc8429bd30bbf8c179f4234078851f9492", size = 128717, upload-time = "2026-09-15T19:30:38.904Z" }, - { url = "https://files.pythonhosted.org/packages/95/0c/a71501bbc1a674ff72c4d6c2b75f4d9a5af819f5244c3a7558080a8802c5/yarl-1.25.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5add7b4ca7afeea91d52e4d4e4db3b1fe9885b71f07054560d8c4296b7441a2", size = 117728, upload-time = "2026-09-15T19:30:40.809Z" }, - { url = "https://files.pythonhosted.org/packages/e7/6f/c3267ca01defeed9ed9c4ff9b17bd54915432c405945233265b707475d1c/yarl-1.25.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:def538065f9e4d4cf1ae164bd59aba00dfa84f03923e0de4c3788f252d6bcd17", size = 116224, upload-time = "2026-09-15T19:30:42.818Z" }, - { url = "https://files.pythonhosted.org/packages/8f/69/fad57ee52d648431718ee0f1f68966a99c1352c3924688ffbdcc9d3fe51a/yarl-1.25.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a191bfdb30a79b98e5d175d75285f9fcb78bf0e46ba5efda042e1c72071a0de", size = 116299, upload-time = "2026-09-15T19:30:44.966Z" }, - { url = "https://files.pythonhosted.org/packages/2a/d4/6a8c1e29f33338687ca278cba0a8fbf6525a322c2c02a9a500ccbe041152/yarl-1.25.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:71f42c5b9a948c113bbdebfa544598321431d064ff959d32e99b1feb61d68345", size = 108625, upload-time = "2026-09-15T19:30:46.874Z" }, - { url = "https://files.pythonhosted.org/packages/e8/d2/3a35ae791c9cb6522c106923ff25c3230d999091e5e65511bca23bbd9914/yarl-1.25.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:72849d892954be4d09e569b8b831ac39ce58417fedc767d4308a0fe542018a40", size = 124515, upload-time = "2026-09-15T19:30:49.082Z" }, - { url = "https://files.pythonhosted.org/packages/be/fd/2b022109a6b4af0f7dc371cf7500af380b0d4f034010243e1b0ce218dc93/yarl-1.25.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:efb01a106f971cb3752856bca2318bbdf7f01bd8823779c461586cbe5ffd5258", size = 115711, upload-time = "2026-09-15T19:30:51.208Z" }, - { url = "https://files.pythonhosted.org/packages/18/59/f7586271136c3ddb0126bbfe661844699369b76b555fe38c4efe86870b2e/yarl-1.25.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:a1daf47cd95a7c3a63456336bc5aaa8c86dd3a47d07ed3d0e76132ae4666a5a1", size = 122751, upload-time = "2026-09-15T19:30:53.535Z" }, - { url = "https://files.pythonhosted.org/packages/a0/0b/07f7a2d881f7e16c385b47fc1753382600847cad305dcc7fa0c25828acf8/yarl-1.25.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9489e6abf47ba37f332075a91444c7cfedb03e6ce99fbb2f116bfe1ce810da3b", size = 117983, upload-time = "2026-09-15T19:30:55.277Z" }, - { url = "https://files.pythonhosted.org/packages/aa/9d/8cdceec66a9b940700cb45931741403f045b162afd79bcb93c41cadd0972/yarl-1.25.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7306dee25b8a0e737363f347362b875094b4dc4e367311470656ae420fdbf8e", size = 102894, upload-time = "2026-09-15T19:30:57.606Z" }, - { url = "https://files.pythonhosted.org/packages/17/f1/7ec357db1d3ad2863542d71e8fe64a126bbec17b134cd7d30951196809a5/yarl-1.25.1-cp311-cp311-win_arm64.whl", hash = "sha256:abb1384477f5901d436b5d2e5465954de46ea6098f59163d243660b5c4461d35", size = 98609, upload-time = "2026-09-15T19:30:59.705Z" }, - { url = "https://files.pythonhosted.org/packages/75/b3/cd32ac66ae622b854c2df0ac52106dda220d361b65a64fde7d5b3684aa3f/yarl-1.25.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:94d7aa6debf92a1dd14cb5280b083a764169a13cfb23a452111160274ed989f4", size = 144798, upload-time = "2026-09-15T19:31:01.821Z" }, - { url = "https://files.pythonhosted.org/packages/61/fb/a2c52a8007c2051ba74662afb112ecf3d00346af4c25e33df9d80fd14fb8/yarl-1.25.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:83d4a37e4b95da4d8bda930d6d35b75b4cdadbacbb4980cae290ea3100b5d51d", size = 104583, upload-time = "2026-09-15T19:31:04.05Z" }, - { url = "https://files.pythonhosted.org/packages/be/dd/ee38aec8e09fdf957e50d4085453fbe202f56c6c3b4cf07b81cdb4f09ee9/yarl-1.25.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e029648f9c951db30e98a7d7ec90835db88ec4b32820efe2a9bdc2287e032eb6", size = 104325, upload-time = "2026-09-15T19:31:06.338Z" }, - { url = "https://files.pythonhosted.org/packages/1e/b3/058dbfb1857b484c9cf9cc135659f50b85ce66e03c99e44dc2f7b6161f55/yarl-1.25.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4d781294bb815ecb5ea57ff6bbf8038e0a31a95fdf3e1788f66e0dc100d64b58", size = 115358, upload-time = "2026-09-15T19:31:08.593Z" }, - { url = "https://files.pythonhosted.org/packages/db/39/29693446cf0cf6b15a0e2f75a5d40f93c56819b05b0622196f45e95b5cc0/yarl-1.25.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e12c538e00e7c1b286a07061046b90e8124e6a9793efae2c70db6a4aad07faad", size = 107658, upload-time = "2026-09-15T19:31:10.802Z" }, - { url = "https://files.pythonhosted.org/packages/86/b3/3c4dd7e1af43b931fba95e0a722737f2ea94a6d199c802585282831d7abd/yarl-1.25.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7e4de3ac4adbad3d0bc7c6f4360a7dbff5de2f15e3b723be3198074e17fd9c40", size = 122660, upload-time = "2026-09-15T19:31:12.84Z" }, - { url = "https://files.pythonhosted.org/packages/bd/b5/1b60dbc3cfc9c5712b15148c206748f2bc93953ffdbe25ea75b63dfc89c9/yarl-1.25.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:419f392a1da624877975709e3864dfe833af6cc7671b39318086d456e288380c", size = 126506, upload-time = "2026-09-15T19:31:15.088Z" }, - { url = "https://files.pythonhosted.org/packages/bc/7b/ca212cbe170ac8b96e45317ecbcf9c3c3ecf0cdec98d5b088a9c4088929b/yarl-1.25.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c6f117789d22dce188e5754e8bc65b7e6ebf8cb73963b9fa761f672a5883769d", size = 117050, upload-time = "2026-09-15T19:31:17.241Z" }, - { url = "https://files.pythonhosted.org/packages/cb/c3/72b4938cdbe619ad71ac156182faef4908846b84dc3ca4dbb4c4e6f84014/yarl-1.25.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:80e47012e730da131c9f059c80936783f9659aae22dc31c03c0595590d11ed54", size = 114174, upload-time = "2026-09-15T19:31:19.294Z" }, - { url = "https://files.pythonhosted.org/packages/e8/43/268717870f9ba0cc9701a95181587f6dc8c5f387aab4aeecc83158f38a79/yarl-1.25.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e80f557716fd765439577131e526b8942ffc2c07bdbc5e39fa62f660ba1e963f", size = 114944, upload-time = "2026-09-15T19:31:21.414Z" }, - { url = "https://files.pythonhosted.org/packages/da/84/baa5bf504d51fe062c4bcaf62936da97fffb43285978d0b39984824231fd/yarl-1.25.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:f61964f235a43738bfac50da46fc4254943a7eea3051aeb0b6fc7c992c29fadc", size = 108263, upload-time = "2026-09-15T19:31:23.388Z" }, - { url = "https://files.pythonhosted.org/packages/a4/28/779a2ed9e0152a601a27039bed9aead3f0b79797a67e2c44bfa444622dd8/yarl-1.25.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e546fe1d4a93ebc2910f0d768baff19faa09843ab3f2036a67ed6e69fae4419d", size = 122184, upload-time = "2026-09-15T19:31:25.343Z" }, - { url = "https://files.pythonhosted.org/packages/f8/1f/118e9e5b8f07694d63fd3222e801d7782270003f1a222aa798df3f8d5933/yarl-1.25.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:cce0727fd5ac04d372fa9bbfde9febc2bcf209aadfcf0468e45dec72719895d1", size = 114001, upload-time = "2026-09-15T19:31:27.465Z" }, - { url = "https://files.pythonhosted.org/packages/0f/ae/a4cf1cf372313734b17996d4007f9f73596e7a178b9485802e5494ecf484/yarl-1.25.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af4ea5b37403ef4e30f3927eaed540db942bde01d8d3ff083527c0704d1c9c68", size = 120565, upload-time = "2026-09-15T19:31:29.47Z" }, - { url = "https://files.pythonhosted.org/packages/05/79/ad94f93ca731bc9e44d321833ab96b82a4f9f5f63cf773f81a4aeea5ecc1/yarl-1.25.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:68782fdb4027b8d1eee25ec35e9a6db05e863b899eb0310b3a33b6c3fef55707", size = 117060, upload-time = "2026-09-15T19:31:31.367Z" }, - { url = "https://files.pythonhosted.org/packages/bb/cc/51a7b4abf4ac593b8e7eb3794b28e5a35ae26eed8bc04787628d215af82f/yarl-1.25.1-cp312-cp312-win_amd64.whl", hash = "sha256:7d575b54cb3863ef9bc290ea4b009999d55dc237326131e4853cf33e888fee03", size = 102593, upload-time = "2026-09-15T19:31:33.329Z" }, - { url = "https://files.pythonhosted.org/packages/9d/21/0941a6b93a58b59a1ec75e5333bf06929b671309c43c0cd201c172d9c39f/yarl-1.25.1-cp312-cp312-win_arm64.whl", hash = "sha256:bc3ac7bf569f6b64dad04dd7808c7872dae8a97df657856eac05e9b7e3614a85", size = 97697, upload-time = "2026-09-15T19:31:35.855Z" }, - { url = "https://files.pythonhosted.org/packages/7b/ed/2f3129bbcc9a5c8ba12cc2b29d8060a3bab9c8043c456cfd4b5ca3188890/yarl-1.25.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:25868beca8b6765f8f7d0e11fe6dd7c66dd4b0793b9500286d20cc92352126a5", size = 143623, upload-time = "2026-09-15T19:31:37.966Z" }, - { url = "https://files.pythonhosted.org/packages/17/e1/f1bc3390fdca352826676b531d0712736f156919090206700421d46b2c37/yarl-1.25.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:10b2fd95332f0d716d5eee3c9fb2ce8eada19082de7fee83d32e37992fd75c26", size = 104011, upload-time = "2026-09-15T19:31:40.25Z" }, - { url = "https://files.pythonhosted.org/packages/a8/aa/50acc5c3e5da04172ae3c281c75405af4d2ca911e16120ab0563f4dffb66/yarl-1.25.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0f12afda4eea8c8994a76d4df1875c765194f5fbe8a9d197929ea303caee29ec", size = 103677, upload-time = "2026-09-15T19:31:42.46Z" }, - { url = "https://files.pythonhosted.org/packages/30/d2/7d1e0ab9f8390e1fbcede5a6dbf70d23c96ad09b8c5567f3a514d1ddb0e2/yarl-1.25.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:14b79a30a93a3ce2e8832603fd0ab780ada281b0ba5110b519a634f2d7d7d1fc", size = 115392, upload-time = "2026-09-15T19:31:44.371Z" }, - { url = "https://files.pythonhosted.org/packages/71/e1/5ba1e3a2a22139213655e760919038e8ed7e2d4a99826d0bbddb3beb96e5/yarl-1.25.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4bd6340d20ae2c7ca719b87b426e808e90743b676d05d4c26c4fb5ca71f41184", size = 107493, upload-time = "2026-09-15T19:31:46.273Z" }, - { url = "https://files.pythonhosted.org/packages/f5/53/780653d5e0f73831f467cf13548912e5eec97f21dc49fc8daf21da027df4/yarl-1.25.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:126a2533570c554719ca40a1288fdee1700b6bc82e7131aa69fa85252d92e651", size = 122537, upload-time = "2026-09-15T19:31:48.654Z" }, - { url = "https://files.pythonhosted.org/packages/03/92/d54fa70236c6036271c9c9c09fd978df5cbe3ef49ef6c46e9b833476d215/yarl-1.25.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a3faadac7d812ddac258feb57b9846b60c1b437c4f4b9ad42595c6f6fe4390df", size = 126170, upload-time = "2026-09-15T19:31:50.872Z" }, - { url = "https://files.pythonhosted.org/packages/0e/b7/a82a49bf88340b837ef6972b508a1604ae377b9e6904b46b10cf5f1cf925/yarl-1.25.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:be80550d9bfe83d9b62398a37081a90434e6df2d978ec345c3d2820de6beddab", size = 117012, upload-time = "2026-09-15T19:31:53.189Z" }, - { url = "https://files.pythonhosted.org/packages/ef/78/5d684b411e3f3602464ee9b538db48205038f8605872985f61efb809ced0/yarl-1.25.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e07595c7d6f4db270ceede356a1bd1c07a34f1c26f958d1ed0cd7b48e0d2bba3", size = 114950, upload-time = "2026-09-15T19:31:55.694Z" }, - { url = "https://files.pythonhosted.org/packages/2f/11/51d82b852c64f7fad0fc7a7ff3031517204887e874c722bbca839c0b23ac/yarl-1.25.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:eb96ed1ae6c7d072d60840c0434aef07a2df611812810807fbc54263a6053e9a", size = 115428, upload-time = "2026-09-15T19:31:57.966Z" }, - { url = "https://files.pythonhosted.org/packages/e4/49/9d1978049bf646b9ea918313926453c6901b71c92f097467777d47d36a88/yarl-1.25.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:3feb99222553a8cbedfa52c2f59dd84c3f50d5b582c728d522caf8d72769a54b", size = 108428, upload-time = "2026-09-15T19:32:00.048Z" }, - { url = "https://files.pythonhosted.org/packages/43/35/7b8f1ebb45d7ec3dda7d1909bf44f458de41ef91e2937f107733582a5166/yarl-1.25.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:a2ed0ba415ccdf08f14bf544cb78346d0f76086707ffee24921a2c84dbf1305a", size = 121961, upload-time = "2026-09-15T19:32:02.436Z" }, - { url = "https://files.pythonhosted.org/packages/63/d6/d8b689ab7ca26edeb85f6ff28812aac7a25376eefc1780e303a7bfbaceff/yarl-1.25.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2b49375d22299b0a834c2bca72f39aaecc270d96fb24c30424899676f487b22a", size = 114961, upload-time = "2026-09-15T19:32:04.456Z" }, - { url = "https://files.pythonhosted.org/packages/cf/d5/1a1798ea4dc6b7ee3260010a27907ebc697c95dae99817d817ed446d24aa/yarl-1.25.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ef74070ac553c59eb4f04258722066d6c6135b7baa03b2e9f2da65c096e96d98", size = 120036, upload-time = "2026-09-15T19:32:06.5Z" }, - { url = "https://files.pythonhosted.org/packages/91/8d/b1b35ed7903da6669b1d367cb2c09436acd4ff508029b4f39a0c0c2058fc/yarl-1.25.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0a66db89ea473abeac4b70523cafd94db3772380e565f9d28af7a179b7af71fa", size = 117276, upload-time = "2026-09-15T19:32:09.401Z" }, - { url = "https://files.pythonhosted.org/packages/3a/8f/4db01cef62caff0d7a4593ed694fb8a41a27a11158cab80d290221f13e57/yarl-1.25.1-cp313-cp313-win_amd64.whl", hash = "sha256:1f51020b2eb8a003c84925638ec63c21a750a4bddd3a22ec8eac6a742dadf1b9", size = 101945, upload-time = "2026-09-15T19:32:11.545Z" }, - { url = "https://files.pythonhosted.org/packages/c0/5e/3ce00497c5c0babb74d4130c10c3828ccd215b4819d12020c42429f991ac/yarl-1.25.1-cp313-cp313-win_arm64.whl", hash = "sha256:b10dd0557ba422715b5206b3743192135a6022acca8baec51aa127d0a75db8fe", size = 97270, upload-time = "2026-09-15T19:32:14.127Z" }, - { url = "https://files.pythonhosted.org/packages/80/cf/54023edfab7aa773b860503db0c56e962ccab0922803ee97988c176ea090/yarl-1.25.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:a9ca696eb02e5c02a8afd872ada510eba9b7fe6e68b9572c2e9a9b1941e31e2e", size = 143975, upload-time = "2026-09-15T19:32:16.416Z" }, - { url = "https://files.pythonhosted.org/packages/d7/a8/e6c1be0e6761d0f2d10bbf33a3e1e02b99dc83874d92945d7b461a72481e/yarl-1.25.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a5877f2255aab518ebe528289037699201d5dc5f045f2396cb30aa02db22f57f", size = 104018, upload-time = "2026-09-15T19:32:18.364Z" }, - { url = "https://files.pythonhosted.org/packages/6e/bb/dda344765ffd3430afe1a1c66c866a57fae67786537d4f14607df6505ac1/yarl-1.25.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7a5c3115595995779ee21f2567035793911c3802a43c74f3fbb0314929ec67ac", size = 104156, upload-time = "2026-09-15T19:32:20.459Z" }, - { url = "https://files.pythonhosted.org/packages/e5/5f/ed1538bcd06009fe990d6d283dd7667f639e62a81e35c6d8c6ef6c08fb3c/yarl-1.25.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77e5099b99b37f3cf79c246998ca9f7313a78054cd1809ec46bc1afad47e1c4c", size = 116025, upload-time = "2026-09-15T19:32:22.766Z" }, - { url = "https://files.pythonhosted.org/packages/a2/af/2185daf56b99830d3356ecfada46faaa49945de6626e842b7728088d4980/yarl-1.25.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6efaf45df6a849cef613a03a94c845647456662f85438c886bb67a9c027c8c2c", size = 106985, upload-time = "2026-09-15T19:32:24.749Z" }, - { url = "https://files.pythonhosted.org/packages/c1/65/bc1ae564fb4b04a30b6a8f250e787772581c57e4c3d5cf07ac3359de3103/yarl-1.25.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d5f90e44653c4e0f78501ed9bb7d3fce835a8d62b7c6ed0cb16557534087e743", size = 123030, upload-time = "2026-09-15T19:32:27.084Z" }, - { url = "https://files.pythonhosted.org/packages/6a/3e/e2afcde10d74e53b3fa889960991efb3019beda2b1682a01de720a302056/yarl-1.25.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:632da579b2d879f6bad20f2cfa35ded1efe2f4f77f8abb26a6234a5b236acd2f", size = 126765, upload-time = "2026-09-15T19:32:29.332Z" }, - { url = "https://files.pythonhosted.org/packages/a2/be/415b00c0fe5a0615b062a456b26623d7ec91c2bee20faea1a14045aa0469/yarl-1.25.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:30eec96e8a91bd588ce897c9543f6d5d8d34b28fbcba28a4dedf20ebeae9fe57", size = 117199, upload-time = "2026-09-15T19:32:31.49Z" }, - { url = "https://files.pythonhosted.org/packages/97/27/3d8c63ddd3e8bcfd033748ab93876678ce59bacd66e4cb1ed851c9c5b37e/yarl-1.25.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:12b6bc4906e11f5e1a1cdcb12296e7afbd366c783cc8073403cd2fb74334e453", size = 115187, upload-time = "2026-09-15T19:32:34.137Z" }, - { url = "https://files.pythonhosted.org/packages/39/b7/7a81d0be1a502a26a0d4326c6f2ecb736c824f570ea1c6529f2b0b227b50/yarl-1.25.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9d6ed3d17bccce4c05343e1ca8da13bc5c02c812a4e7282ddd05e8769322d3fc", size = 116085, upload-time = "2026-09-15T19:32:36.438Z" }, - { url = "https://files.pythonhosted.org/packages/f0/69/39fff459916aa0fab42215dc47b759586fd80f94aa56dfc4a7c15ba6e0dc/yarl-1.25.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:f38a70074041d3b7e138e452799f5174198bae5bd5ab2000917badf403908c5f", size = 107996, upload-time = "2026-09-15T19:32:38.959Z" }, - { url = "https://files.pythonhosted.org/packages/c0/39/80b9a55a3335590451d9ecf3eb593a8c635351f4c905ef056d7e8a8fd9e7/yarl-1.25.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:4ca89e4e21854ed27ec753297dde84b16c9f8e53b14a4866fb44457d643c19f8", size = 122549, upload-time = "2026-09-15T19:32:41.151Z" }, - { url = "https://files.pythonhosted.org/packages/42/7d/a179c6757818bb59372a4adafd09f7f26a3b4a0f04c3ae404b544c0b0c82/yarl-1.25.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:1ab7618921a93767387a4b83776f751588f5b5ae9bb5bc96620e2e2e00bca868", size = 115107, upload-time = "2026-09-15T19:32:43.072Z" }, - { url = "https://files.pythonhosted.org/packages/32/2b/a773ac867e4ab53a98ed98e5cefe3bae31e6f550252ca9d1de266f1a40c5/yarl-1.25.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:0ae12ff2b805fa02c4dab838005caef735e39986322698c48588d3beacb65c62", size = 120666, upload-time = "2026-09-15T19:32:45.061Z" }, - { url = "https://files.pythonhosted.org/packages/bc/41/52be6505e85b0f76b4f85b01b5de7e06a0512201abc2c95e14e099549174/yarl-1.25.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:90c30ed53546da833c700115c0064c22120d1b1560f474699fd31f22dd668233", size = 117505, upload-time = "2026-09-15T19:32:47.177Z" }, - { url = "https://files.pythonhosted.org/packages/f5/01/349c0386caedbbe488d519f252df54efac8a1459282d466c474bdd84a620/yarl-1.25.1-cp314-cp314-win_amd64.whl", hash = "sha256:acfa7e22aa6c6e7a5996a41d275bfa01efa7ea56ab890590280e9063e2cf5c1b", size = 103446, upload-time = "2026-09-15T19:32:49.615Z" }, - { url = "https://files.pythonhosted.org/packages/5c/f0/8ec63180f77912f0dc4e5a42760cb8c08d20da1d5ace3578a01b84d1f3d8/yarl-1.25.1-cp314-cp314-win_arm64.whl", hash = "sha256:8e7d98cdbb6d71e726f7d525952867096053d1f290dd4e3c50d7d313a136f414", size = 99159, upload-time = "2026-09-15T19:32:51.686Z" }, - { url = "https://files.pythonhosted.org/packages/47/7d/92d2220d6886b70ab1ed8579533ac2af2dfac716d5d929001daff7986df9/yarl-1.25.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:d21f0fa80a02d05299207eeaafef345d812ace96d5306e4ef265e1d419a615fa", size = 150071, upload-time = "2026-09-15T19:32:53.911Z" }, - { url = "https://files.pythonhosted.org/packages/64/fc/b245e448124bcda9340df38e3553fa222b50260fca027a84095e9bd8642d/yarl-1.25.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:17c9877a89fb6e2bca6f9087eb24cd7fb434653946ef5075e470d23d49b52287", size = 106780, upload-time = "2026-09-15T19:32:56.443Z" }, - { url = "https://files.pythonhosted.org/packages/51/e2/9a6ce2e334ebf218a30335ae76fb1696459430d42f733b8cb0d7d65b84d3/yarl-1.25.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:29273edf1530e397bd07cb784db1fbe0d2590b77569f2e24679a9c0a2d763b94", size = 107361, upload-time = "2026-09-15T19:32:58.827Z" }, - { url = "https://files.pythonhosted.org/packages/ed/70/66e8c76b569b450d16e190f15071c916c3df70b0e33927e415ac497cf0c2/yarl-1.25.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b7abffdf37af1cec6a2ad69b827aa84320db5894791bc8ed932dc93fb274b7e9", size = 114396, upload-time = "2026-09-15T19:33:02.24Z" }, - { url = "https://files.pythonhosted.org/packages/73/23/0d82838a05c57fdc05bc8b66e8c92dcc0df15e27463a5f163142d521c682/yarl-1.25.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2239a02249d9326655419e0168a28ca9008938eaab31dc29fc875c217927a6c0", size = 104882, upload-time = "2026-09-15T19:33:04.494Z" }, - { url = "https://files.pythonhosted.org/packages/86/d4/ea08615c4edaa6049a13a2f1128944d068d1893abda7d708d4d7ea01599a/yarl-1.25.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:664ec6a520b74a1df2810666eb67695fcb77fa663e6ea0a25aaf2e529cb24dfa", size = 119485, upload-time = "2026-09-15T19:33:06.583Z" }, - { url = "https://files.pythonhosted.org/packages/1a/82/0898bdce9b1ae403b308b9c733d0d24af4a3464270c2c081f457b16c3e0d/yarl-1.25.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4f1c91f5a5980a937ff8e238e98e6897e1ad74a4b1e2c0d68c73b5ffbb3f5c0b", size = 122490, upload-time = "2026-09-15T19:33:08.653Z" }, - { url = "https://files.pythonhosted.org/packages/d1/38/97d79b81c342b78246cfedb74809e68841f3198d21653e10d3232bd9c622/yarl-1.25.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c88edaec8c349ad4c5ad4c486a3defcc4b80ceb2f074436ffa0a87caf5e76a6", size = 115336, upload-time = "2026-09-15T19:33:11.056Z" }, - { url = "https://files.pythonhosted.org/packages/8e/9d/2577896554cd310dc470adb6da0b7dd0b435cb63e2565204a7ac240e504c/yarl-1.25.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:35dcbea443fafb3eece757ad4e514560ddeb6c34cfae1582c620d7b293d7feee", size = 111825, upload-time = "2026-09-15T19:33:13.204Z" }, - { url = "https://files.pythonhosted.org/packages/29/6b/7ac49d8ba84a5c4bd73415a4c949d22c749cb3762579b3d50e48019a78aa/yarl-1.25.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:882569ff613758cac762a457a5d72d6e211b28d4bcfea89d1d71ea942b02eac0", size = 114655, upload-time = "2026-09-15T19:33:15.553Z" }, - { url = "https://files.pythonhosted.org/packages/e5/18/e5942a16723f5b72f9b1297fd5a85a54f6300cd15c0dcb5005b90cd89156/yarl-1.25.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:d0f1489233a254bb3643d2f05de7d59019254d81daeca6b9162fe9edef57e0c7", size = 106395, upload-time = "2026-09-15T19:33:17.599Z" }, - { url = "https://files.pythonhosted.org/packages/7b/2d/549fa46240781513ebc47ae7eb418df428a163a2a3d644cc9cbb3ecb7846/yarl-1.25.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:f41753a76f4f63927d03a0d8ba8f5ce0f2083bec29a8cfaccc55371b1564b96b", size = 119277, upload-time = "2026-09-15T19:33:19.973Z" }, - { url = "https://files.pythonhosted.org/packages/76/16/4763f78dcdc0b3b9fb3842b04afe72b9320857c6a69300c62a0eab03d119/yarl-1.25.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8fb0eb4955adf0579001581f2f71a126e8781ba61bcd120f127b0401163c6c2d", size = 112504, upload-time = "2026-09-15T19:33:22.464Z" }, - { url = "https://files.pythonhosted.org/packages/ae/b4/974e3edfe0d188393ce1cb9de400111c63fe61f4eb3b772a500d84c970d1/yarl-1.25.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:a1e32763e641a1566507d90a8d3b19bfc3cc04a9d4e5ae3e32189874ed4b58a3", size = 116243, upload-time = "2026-09-15T19:33:24.788Z" }, - { url = "https://files.pythonhosted.org/packages/b0/aa/157b940428da80c104ca09666a740e51c94963df65d5b112e06b52e4d7a8/yarl-1.25.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:65b5b2066651b7432d389e9799d979c703bcc6ef44266bb8153ef54e91e4aab3", size = 115822, upload-time = "2026-09-15T19:33:26.886Z" }, - { url = "https://files.pythonhosted.org/packages/7e/af/19fbdce41412e1b96825544cc52cd7029d3724655d0988237972f078bd29/yarl-1.25.1-cp314-cp314t-win_amd64.whl", hash = "sha256:734f6e5400352ac4254456003d462866c684703570929cff7a7bde015d0cb371", size = 107386, upload-time = "2026-09-15T19:33:29.009Z" }, - { url = "https://files.pythonhosted.org/packages/2a/99/f6431c8968e89be608d74b28ae2d024521b2953f27dd44e0dece5e04f67a/yarl-1.25.1-cp314-cp314t-win_arm64.whl", hash = "sha256:287e99ff5aa4dc1c7630bfc683ded6f106d756c99dec432a2d7f197a784f51c6", size = 102094, upload-time = "2026-09-15T19:33:31.151Z" }, - { url = "https://files.pythonhosted.org/packages/c7/3b/4f51eab40c2eabea6c3d5b121dff4b8988dc35087732ffede12d2be8b8dd/yarl-1.25.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:9b1bdaae98bc016825dd3c9d8ee1832f829b3341f9cc6ebd1a1b0a7fef7367cc", size = 143875, upload-time = "2026-09-15T19:33:33.52Z" }, - { url = "https://files.pythonhosted.org/packages/41/05/bbd58fc063f5f299a883f810760b265ca26c8167c91cb9a494d0fe2387e1/yarl-1.25.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:e7011b8fb8c4054bf0c12e5edc6cd83778b0028e99ce59b18586ed036f92cfdc", size = 104028, upload-time = "2026-09-15T19:33:36.22Z" }, - { url = "https://files.pythonhosted.org/packages/12/ee/2fba0aecb52e7020e189f684148783aa0b9cfa3b3bfb0b400646eef70ad4/yarl-1.25.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:f074e8d4aa0a5798920ddb6de3d08b228c614ff3724c3e8bd7577f4bafea867b", size = 104041, upload-time = "2026-09-15T19:33:38.86Z" }, - { url = "https://files.pythonhosted.org/packages/38/35/884beab53ed88c7247d1671972b5ef116f7351fe0c7e6de8c3558372cb16/yarl-1.25.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d42e7e3ca399555578b4d617e3a6ecf13371b3743a115995fa010c7bf341459", size = 116018, upload-time = "2026-09-15T19:33:44.265Z" }, - { url = "https://files.pythonhosted.org/packages/e2/cc/1a91b685afb55cc18608443ace95280e96e263a97565811732b6788d3269/yarl-1.25.1-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:aa4ed3dd308548f9e707d9caaf005d2d7f8c1e7868f858dfeb47fe76e16b391d", size = 107033, upload-time = "2026-09-15T19:33:46.45Z" }, - { url = "https://files.pythonhosted.org/packages/c5/c1/58b379fcb1d68d907b7fcf75200c44321896509b2a6a74abbb4b19d864d2/yarl-1.25.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:42a66563d8cc056ee32e6191e05097a7b2b3bc302e0bc3133daf8710eb18bd26", size = 123257, upload-time = "2026-09-15T19:33:48.631Z" }, - { url = "https://files.pythonhosted.org/packages/d7/2d/1fe96cf5c2aeab10095e48f38585cf5a8451fb7253234822398e52aa5336/yarl-1.25.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:98d370568f393215d605304cdb77b3d5539bd192c75b623c7304c42c8d6d8273", size = 126745, upload-time = "2026-09-15T19:33:50.999Z" }, - { url = "https://files.pythonhosted.org/packages/ad/60/8674394ce43f4dadae573a1d6f451716438e9eab7d7fe8d643c673a32d85/yarl-1.25.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23bf5b403c879a54964e0feac7285688e04bb220074878d737d331522da0a5bf", size = 117255, upload-time = "2026-09-15T19:33:53.456Z" }, - { url = "https://files.pythonhosted.org/packages/2f/72/0faa30e02605d56127d42bb987dcc97da3863b7bf70b9bfbf5f739c05e30/yarl-1.25.1-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d45673badd08456d0340e9364eddafe1c53a9d2896424294de4d7dd71ad3ee57", size = 115166, upload-time = "2026-09-15T19:33:55.669Z" }, - { url = "https://files.pythonhosted.org/packages/8c/90/9a46eac564c437e128285c5c1d7bb385d268394e9209d84f6bf4a14471ef/yarl-1.25.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:0136d640dfa9b0523853e411430a99f8a91eca85774c6420285a33b755bc6de3", size = 116082, upload-time = "2026-09-15T19:33:57.78Z" }, - { url = "https://files.pythonhosted.org/packages/ad/38/4b1a686a3758878f93d2f1ea943f5a165f3555769cd16e761cfd0efdca17/yarl-1.25.1-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:59ba3a6e1aa8cfe5adf4bd270fd965db21955401b7ca6f1696010c55ed4daec2", size = 108048, upload-time = "2026-09-15T19:34:00.25Z" }, - { url = "https://files.pythonhosted.org/packages/da/14/f348eb967f31a58348612a2b93bd8a2ab664548e2b5e879cac7f592f7201/yarl-1.25.1-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:87796fedc3ba97ec14fab55acb48584276e6c1e4c1e89c422bda62c838e754a9", size = 122775, upload-time = "2026-09-15T19:34:02.455Z" }, - { url = "https://files.pythonhosted.org/packages/ab/e9/4f7b79700f88cb9e8bb66f8b54f9bce1844c013a2c39fdc47112e9334c95/yarl-1.25.1-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:bd0912757081f89b107d6c00b2ff8a194401b0b87eadcf4481de2b865a8fd44f", size = 115096, upload-time = "2026-09-15T19:34:05.283Z" }, - { url = "https://files.pythonhosted.org/packages/cf/37/f9cb020331997d3eb887bd28d5410ecfd3d80bf163c23d7cec490d78dade/yarl-1.25.1-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:b51c159a9794633f5e0db7ecec7b2b6e3734eca1f5d17dc989ff3552a43ff78b", size = 120655, upload-time = "2026-09-15T19:34:07.382Z" }, - { url = "https://files.pythonhosted.org/packages/12/83/52fceb22891a41f168db7ec22fd1d81e06b6a0b8d9f70921bd3e785defd0/yarl-1.25.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:319e070a01db9920fb63761843f96a104c8e2b9427266731810dc1e22595b17c", size = 117488, upload-time = "2026-09-15T19:34:09.988Z" }, - { url = "https://files.pythonhosted.org/packages/f5/5c/ce6c4ff1247fcbe4b33d462c23a097106d909b173fde7042bc52290466e2/yarl-1.25.1-cp315-cp315-win_amd64.whl", hash = "sha256:a2059a2d891bd156bc5184e7ab7a56e78a84dfcfdeac8c501b552533ad1c36ee", size = 103434, upload-time = "2026-09-15T19:34:12.56Z" }, - { url = "https://files.pythonhosted.org/packages/3c/a1/766a906b0704fb26d52b19dc22bed48a8ba0544203b70dcf44da350e8194/yarl-1.25.1-cp315-cp315-win_arm64.whl", hash = "sha256:a78b50b4f7918a3de71105d5c0b93bbc57bb8339a4d03a9dfd449f9068e76f3d", size = 99155, upload-time = "2026-09-15T19:34:15.132Z" }, - { url = "https://files.pythonhosted.org/packages/bd/d3/a1d09b32cb6ab14f66b44939f5b4255b8b9e747aef3974af1d5d80ccc2fd/yarl-1.25.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:b5402a340723fa7da00b5cff987ddab61276be6d11251ea71ae02bcac54890d8", size = 149284, upload-time = "2026-09-15T19:34:17.452Z" }, - { url = "https://files.pythonhosted.org/packages/7f/3b/fe554d879692650bca70bfbc0df124e82e4d2bb7456f698c7756f1279a96/yarl-1.25.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:eda19ea5ee88742f47a2340816e6f2d40b53bed3ab5b69794769f36af9f35bb4", size = 106399, upload-time = "2026-09-15T19:34:21.474Z" }, - { url = "https://files.pythonhosted.org/packages/e1/4b/e7af56177ac8d40094c82d7728224c0b8472157d50d362e5fb3b014b2bc8/yarl-1.25.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:75baa6cf9b6d1c52f3e111a130e202fd8cf0a5b3a066c3f73d615e885092e4ec", size = 106968, upload-time = "2026-09-15T19:34:23.619Z" }, - { url = "https://files.pythonhosted.org/packages/da/4f/2df41fd738d46f23ef829ae8b4468d94bb6070038fc6dfab6165ed44fea8/yarl-1.25.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dbcef5a9119ef653653132cccaf999b30a0af6f33bb0a4ba80bec30056868487", size = 114717, upload-time = "2026-09-15T19:34:25.879Z" }, - { url = "https://files.pythonhosted.org/packages/69/ea/002b66df53bbd1aed1c23358ff99c9bdc744fe3f4d2740e1b2fdd7192885/yarl-1.25.1-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7efc9f082dfed77c316edffa9deb52888e1bc6789171887cc1f68e06d65465c8", size = 105198, upload-time = "2026-09-15T19:34:28.204Z" }, - { url = "https://files.pythonhosted.org/packages/b1/7c/95c8bc0c8f97d71e59c94525ad60d76f5c57d3f2820f08137ca8b9f0542a/yarl-1.25.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fe01645169a2112aa1d4ebc3e4c5f029c5c8f97adfc32e5d37c993b39a994d75", size = 120271, upload-time = "2026-09-15T19:34:30.5Z" }, - { url = "https://files.pythonhosted.org/packages/5d/7a/6fe9da56ec77927baa669fd86c39c567ce6205bab53d581082c6744c8ae7/yarl-1.25.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1c557dfd5e3db046053a0bdc72261ade790ebe8e2c7a41b36b0ca1f14cb95f3", size = 123572, upload-time = "2026-09-15T19:34:32.73Z" }, - { url = "https://files.pythonhosted.org/packages/8b/83/35f222d17fa70a14c7c74fdf112ccf5515e0c2a87082b1f9b99f7693bf57/yarl-1.25.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ce4d6ccafb33d39bd78444612d14938ead674c25702ded2ee9c54a47735d225", size = 115228, upload-time = "2026-09-15T19:34:35.344Z" }, - { url = "https://files.pythonhosted.org/packages/da/6f/fbaaf619423578a7d898d0f226ae47bc1906c293865c416d83c17b828b0e/yarl-1.25.1-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:80a063f8297fc796296f00f100be520f209b23dc98f93ce8eba6ee7122598209", size = 111618, upload-time = "2026-09-15T19:34:37.656Z" }, - { url = "https://files.pythonhosted.org/packages/ba/78/7383278f1b3cf8e0496bd95b3281a7b09b89217b6b428db24c6b99b3deca/yarl-1.25.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:7cb414a73e21a7ab58254926073f2930cb22f5b4314ea4260a687e2b3fd4dce3", size = 114839, upload-time = "2026-09-15T19:34:40.099Z" }, - { url = "https://files.pythonhosted.org/packages/62/49/5506e5b6d29aab91bd845cc9016d88c3d3f81b81bc242b8100bdd5737825/yarl-1.25.1-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:85a18376073f8a39aa07be34f9fc77e2869aa72c55c441efdd2cf79a0407504d", size = 106212, upload-time = "2026-09-15T19:34:42.768Z" }, - { url = "https://files.pythonhosted.org/packages/46/8a/19877c193b7c5929f4b07118c18bbe390f3fadd0f59dd98c0cd12b31fa5c/yarl-1.25.1-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:77716e245c90f058466a05e6a465bb8600f767a8f4b18b4d40f3aff958e5f73c", size = 119985, upload-time = "2026-09-15T19:34:44.976Z" }, - { url = "https://files.pythonhosted.org/packages/4e/6c/0a46fbbf9ecbcbd0cc20d2193394254b9e19817f22c814aab60f99847400/yarl-1.25.1-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:1e80dcf1446e1b080b1932b0d103c464a04112f5bc31f0f983ad418172063cde", size = 112081, upload-time = "2026-09-15T19:34:47.45Z" }, - { url = "https://files.pythonhosted.org/packages/ef/30/93f5d471230c74ccd06255d0842739f86551f937f9e63a5e947853c6244a/yarl-1.25.1-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:bdc8d8b8c22e9e43ac68316b5e6cf083dec537f4ec213cb4aa967b583bc3fa64", size = 116995, upload-time = "2026-09-15T19:34:49.972Z" }, - { url = "https://files.pythonhosted.org/packages/2b/80/c386593035ee3f9c6c6af0847b5578f2830c674794a9d7701b744a3ebd42/yarl-1.25.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:dfbf531053a0935f2e871bcd4753f90313688772ff8c017f5ea402e315a78c1f", size = 115570, upload-time = "2026-09-15T19:34:52.628Z" }, - { url = "https://files.pythonhosted.org/packages/38/02/eef443559563ef8f2e10469387b8b1e97cb5efee95b288a56da60801f7ee/yarl-1.25.1-cp315-cp315t-win_amd64.whl", hash = "sha256:b13b88747769537f3d32e89e3a735da10c0a9e35d7322928c701b5f93d3afffd", size = 106811, upload-time = "2026-09-15T19:34:54.935Z" }, - { url = "https://files.pythonhosted.org/packages/88/91/41e284ca2cf5211e05dae031d126a3668aea88fa759df56e7e35c6ad25ba/yarl-1.25.1-cp315-cp315t-win_arm64.whl", hash = "sha256:783dd1467083f4d3f7722ad6a313f24c173e7571372738fcb7a6e6d1ba48df25", size = 101804, upload-time = "2026-09-15T19:34:57.231Z" }, - { url = "https://files.pythonhosted.org/packages/54/22/318c7980066769c6bcd9221ed2248294f5698811da099013098c670565ed/yarl-1.25.1-py3-none-any.whl", hash = "sha256:681c758b0490f9e96b78e5fa8e8dc6e648e9185bb6eaebe73183c33ea0c445f3", size = 63617, upload-time = "2026-09-15T19:34:59.616Z" }, -] - [[package]] name = "zensical" version = "0.0.50"