From 82feec20cc78da0633c52f3bd09bcb30e76a20e7 Mon Sep 17 00:00:00 2001 From: Stefan VanBuren Date: Tue, 13 Jan 2026 10:41:22 -0500 Subject: [PATCH 1/3] Drop docs/ directory We now have published docs in connectrpc.com repo that we ought to use as the source of truth for connect-python docs; this just drops the docs/ content and related dependencies from this repo. Completes #83. Ref: connectrpc/connectrpc.com#312 Signed-off-by: Stefan VanBuren --- docs/api.md | 9 - docs/deployment.md | 68 ----- docs/errors.md | 192 ------------- docs/examples.md | 36 --- docs/get-requests-and-caching.md | 69 ----- docs/getting-started.md | 258 ------------------ docs/grpc-compatibility.md | 98 ------- docs/headers-and-trailers.md | 102 ------- docs/index.md | 56 ---- docs/interceptors.md | 265 ------------------ docs/streaming.md | 131 --------- docs/testing.md | 453 ------------------------------- docs/usage.md | 310 --------------------- justfile | 35 +-- pyproject.toml | 354 ++++++++++++------------ uv.lock | 385 -------------------------- 16 files changed, 186 insertions(+), 2635 deletions(-) delete mode 100644 docs/api.md delete mode 100644 docs/deployment.md delete mode 100644 docs/errors.md delete mode 100644 docs/examples.md delete mode 100644 docs/get-requests-and-caching.md delete mode 100644 docs/getting-started.md delete mode 100644 docs/grpc-compatibility.md delete mode 100644 docs/headers-and-trailers.md delete mode 100644 docs/index.md delete mode 100644 docs/interceptors.md delete mode 100644 docs/streaming.md delete mode 100644 docs/testing.md delete mode 100644 docs/usage.md diff --git a/docs/api.md b/docs/api.md deleted file mode 100644 index ccdeb8a..0000000 --- a/docs/api.md +++ /dev/null @@ -1,9 +0,0 @@ -# API Reference - -::: connectrpc.client -::: connectrpc.code -::: connectrpc.errors -::: connectrpc.interceptor -::: connectrpc.method -::: connectrpc.request -::: connectrpc.server diff --git a/docs/deployment.md b/docs/deployment.md deleted file mode 100644 index 02d1ac0..0000000 --- a/docs/deployment.md +++ /dev/null @@ -1,68 +0,0 @@ -# Deployment - -After building a Connect service, you still need to deploy it to production. This guide covers how to -use a Python application server to run a service. - -## Application Servers - -Connect services are standard ASGI or WSGI applications that can be run with an off-the-shelf Python -application server. Generally, any experience in running a Python application can be applied to running -Connect as-is. - -### HTTP/1.1 - -Connect only requires HTTP/2 for bidirectional streaming RPCs - the vast majority of services can be run -using HTTP/1.1 without issue. Two servers have long been in use with HTTP/1.1 and have a proven track -record. If you are unsure what server to use, it is generally a safe bet to use one of them: - -- [gunicorn](http://www.gunicorn.org/) for WSGI applications -- [uvicorn](https://uvicorn.dev/) for ASGI applications - -### HTTP/2 - -If your service uses bidirectional or otherwise want to use HTTP/2, the above servers will not work. -HTTP/2 support in the Python ecosystem is still relatively young - servers known to support HTTP/2 -with bidirectional streaming are: - -- [granian](https://github.com/emmett-framework/granian) -- [hypercorn](https://hypercorn.readthedocs.io/en/latest/) -- [pyvoy](https://github.com/curioswitch/pyvoy) - -Connect has an extensive test suite to verify compatibility of connect-python with the Connect protocol. -Unfortunately, we are only able to reliably pass the suite with pyvoy, with other servers occasionally -having hung requests or stream ordering issues. pyvoy was built with connect-python in mind but is -very new and needs more time with real-world applications to verify stability. - -Keep the above in mind when picking an HTTP/2 server and let us know how it goes if you give any a try. -When in doubt, if you do not use bidirectional streaming, we recommend one of the HTTP/1.1 servers. - -## CORS - -Connect services are standard ASGI and WSGI applications so any CORS middleware can be used to -enable it. - -For example, with an ASGI application using the [asgi-cors](https://pypi.org/project/asgi-cors/) -middleware: - -```python -from asgi_cors import asgi_cors - -from greet.v1.greet_connect import GreetServiceASGIApplication -from server import Greeter - -app = GreetServiceASGIApplication(Greeter()) - -# app is a standard ASGI application - any middleware works as-is -app = asgi_cors( - app, - hosts=["https://acme.com"], - # POST is used for all APIs except for idempotent unary RPCs that may support GET - methods=["GET", "POST"], - headers=[ - "content-type", - "connect-protocol-version", - "connect-timeout-ms", - "x-user-agent", - ], -) -``` diff --git a/docs/errors.md b/docs/errors.md deleted file mode 100644 index ebfe698..0000000 --- a/docs/errors.md +++ /dev/null @@ -1,192 +0,0 @@ -# Errors - -Similar to the familiar "404 Not Found" and "500 Internal Server Error" status codes in HTTP, Connect uses a set of [16 error codes](https://connectrpc.com/docs/protocol#error-codes). These error codes are designed to work consistently across Connect, gRPC, and gRPC-Web protocols. - -## Working with errors - -Connect handlers raise errors using `ConnectError`: - -=== "ASGI" - - ```python - from connectrpc.code import Code - from connectrpc.errors import ConnectError - from connectrpc.request import RequestContext - - async def greet(self, request: GreetRequest, ctx: RequestContext) -> GreetResponse: - if not request.name: - raise ConnectError(Code.INVALID_ARGUMENT, "name is required") - return GreetResponse(greeting=f"Hello, {request.name}!") - ``` - -=== "WSGI" - - ```python - from connectrpc.code import Code - from connectrpc.errors import ConnectError - from connectrpc.request import RequestContext - - def greet(self, request: GreetRequest, ctx: RequestContext) -> GreetResponse: - if not request.name: - raise ConnectError(Code.INVALID_ARGUMENT, "name is required") - return GreetResponse(greeting=f"Hello, {request.name}!") - ``` - -Clients catch errors the same way: - -=== "Async" - - ```python - from connectrpc.code import Code - from connectrpc.errors import ConnectError - - async with GreetServiceClient("http://localhost:8000") as client: - try: - response = await client.greet(GreetRequest(name="")) - except ConnectError as e: - if e.code == Code.INVALID_ARGUMENT: - print(f"Invalid request: {e.message}") - else: - print(f"RPC failed: {e.code} - {e.message}") - ``` - -=== "Sync" - - ```python - from connectrpc.code import Code - from connectrpc.errors import ConnectError - - with GreetServiceClientSync("http://localhost:8000") as client: - try: - response = client.greet(GreetRequest(name="")) - except ConnectError as e: - if e.code == Code.INVALID_ARGUMENT: - print(f"Invalid request: {e.message}") - else: - print(f"RPC failed: {e.code} - {e.message}") - ``` - -## Error codes - -Connect uses a set of [16 error codes](https://connectrpc.com/docs/protocol#error-codes). The `code` property of a `ConnectError` holds one of these codes. All error codes are available through the `Code` enumeration: - -```python -from connectrpc.code import Code - -code = Code.INVALID_ARGUMENT -code.value # "invalid_argument" - -# Access by name -Code["INVALID_ARGUMENT"] # Code.INVALID_ARGUMENT -``` - -## Error messages - -The `message` property contains a descriptive error message. In most cases, the message is provided by the backend implementing the service: - -```python -try: - response = await client.greet(GreetRequest(name="")) -except ConnectError as e: - print(e.message) # "name is required" -``` - -## Error details - -Errors can include strongly-typed details using protobuf messages: - -```python -from connectrpc.code import Code -from connectrpc.errors import ConnectError -from connectrpc.request import RequestContext -from google.protobuf.struct_pb2 import Struct, Value - -async def create_user(self, request: CreateUserRequest, ctx: RequestContext) -> CreateUserResponse: - if not request.email: - error_detail = Struct(fields={ - "field": Value(string_value="email"), - "issue": Value(string_value="Email is required") - }) - - raise ConnectError( - Code.INVALID_ARGUMENT, - "Invalid user request", - details=[error_detail] - ) - # ... rest of implementation -``` - -### Reading error details on the client - -Error details are `google.protobuf.Any` messages that can be unpacked to their original types: - -```python -try: - response = await client.some_method(request) -except ConnectError as e: - for detail in e.details: - # Check the type before unpacking - if detail.Is(Struct.DESCRIPTOR): - unpacked = Struct() - detail.Unpack(unpacked) - print(f"Error detail: {unpacked}") -``` - -### Standard error detail types - -With `googleapis-common-protos` installed, you can use standard types like: - -- `BadRequest`: Field violations in a request -- `RetryInfo`: When to retry -- `Help`: Links to documentation -- `QuotaFailure`: Quota violations -- `ErrorInfo`: Structured error metadata - -Example: - -```python -from google.rpc.error_details_pb2 import BadRequest - -bad_request = BadRequest() -violation = bad_request.field_violations.add() -violation.field = "email" -violation.description = "Must be a valid email address" - -raise ConnectError( - Code.INVALID_ARGUMENT, - "Invalid email format", - details=[bad_request] -) -``` - -## HTTP representation - -In the Connect protocol, errors are always JSON: - -```http -HTTP/1.1 400 Bad Request -Content-Type: application/json - -{ - "code": "invalid_argument", - "message": "name is required", - "details": [ - { - "type": "google.protobuf.Struct", - "value": "base64-encoded-protobuf" - } - ] -} -``` - -The `details` array contains error detail messages, where each entry has: - -- `type`: The fully-qualified protobuf message type (e.g., `google.protobuf.Struct`) -- `value`: The protobuf message serialized in binary format and then base64-encoded - -## See also - -- [Interceptors](interceptors.md) for error transformation and logging -- [Streaming](streaming.md) for stream-specific error handling -- [Headers and trailers](headers-and-trailers.md) for attaching metadata to errors -- [Usage guide](usage.md) for error handling best practices diff --git a/docs/examples.md b/docs/examples.md deleted file mode 100644 index efabcc2..0000000 --- a/docs/examples.md +++ /dev/null @@ -1,36 +0,0 @@ -# Examples - -This section contains practical examples of using connect-python. - -## Basic Client Example - -Here's a simple synchronous client example: - -```python -from your_generated_code import ElizaServiceClient, eliza_pb2 - -# Create client -eliza_client = ElizaServiceClient("https://demo.connectrpc.com") - -# Make a simple unary call -response = eliza_client.say(eliza_pb2.SayRequest(sentence="Hello, Eliza!")) -print(f"Eliza says: {response.sentence}") -``` - -## Async Client Example - -For asynchronous operations: - -```python -from your_generated_code import AsyncElizaServiceClient, eliza_pb2 - -async def main(): - async with AsyncElizaServiceClient("https://demo.connectrpc.com") as eliza_client: - # Make an async unary call - response = await eliza_client.say(eliza_pb2.SayRequest(sentence="Hello, Eliza!")) - print(f"Eliza says: {response.sentence}") -``` - -## More Examples - -For more detailed examples, see the [Usage Guide](./usage.md). diff --git a/docs/get-requests-and-caching.md b/docs/get-requests-and-caching.md deleted file mode 100644 index 10581d7..0000000 --- a/docs/get-requests-and-caching.md +++ /dev/null @@ -1,69 +0,0 @@ -# Get Requests and Caching - -Connect supports performing idempotent, side-effect free requests using an HTTP GET-based protocol. -This makes it easier to cache certain kinds of requests in the browser, on your CDN, or in proxies and -other middleboxes. - -If you are using clients to make query-style requests, you may want the ability to use Connect HTTP GET -request support. To opt-in for a given procedure, you must mark it as being side-effect free using the -`MethodOptions.IdempotencyLevel` option: - -```protobuf -service ElizaService { - rpc Say(SayRequest) returns (SayResponse) { - option idempotency_level = NO_SIDE_EFFECTS; - } -} -``` - -Services will automatically support GET requests using this option. - -It is still necessary to opt-in to HTTP GET on your client, as well. Generated clients include a -`use_get` parameter for methods that are marked with `NO_SIDE_EFFECTS`. - -=== "Async" - - ```python - response = await client.say(SayRequest(sentence="Hello"), use_get=True) - ``` - -=== "Sync" - - ```python - response = client.say(SayRequest(sentence="Hello"), use_get=True) - ``` - -For other clients, see their respective documentation pages: - -- [Connect Node](https://connectrpc.com/docs/node/get-requests-and-caching) -- [Connect Web](https://connectrpc.com/docs/web/get-requests-and-caching) -- [Connect Kotlin](https://connectrpc.com/docs/kotlin/get-requests-and-caching) - -## Caching - -Using GET requests will not necessarily automatically make browsers or proxies cache your RPCs. -To ensure that requests are allowed to be cached, a handler should also set the appropriate headers. - -For example, you may wish to set the `Cache-Control` header with a `max-age` directive: - -```python -ctx.response_headers()["cache-control"] = "max-age=604800" -return SayResponse() -``` - -This would instruct agents and proxies that the request may be cached for up to 7 days, after which -it must be re-requested. There are other [`Cache-Control` Response Directives](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#response_directives) -that may be useful for your application as well; for example, the `private` directive would specify that -the request should only be cached in private caches, such as the user agent itself, and _not_ CDNs or reverse -proxies — this would be appropriate, for example, for authenticated requests. - -## Distinguishing GET Requests - -In some cases, you might want to introduce behavior that only occurs when handling HTTP GET requests. -This can be accomplished with `RequestContext.http_method`: - -```python -if ctx.http_method() == "GET": - ctx.response_headers()["cache-control"] = "max-age=604800" -return SayResponse() -``` diff --git a/docs/getting-started.md b/docs/getting-started.md deleted file mode 100644 index 98e6990..0000000 --- a/docs/getting-started.md +++ /dev/null @@ -1,258 +0,0 @@ -# Getting Started - -Connect is a slim library for building HTTP APIs consumable anywhere, including browsers. -You define your service with a Protocol Buffer schema, and Connect generates type-safe server -and client code. Fill in your server's business logic and you're done — no hand-written -marshaling, routing, or client code required! - -This fifteen-minute walkthrough helps you create a small Connect service in Python. -It demonstrates what you'll be writing by hand, what Connect generates for you, -and how to call your new API. - -## Prerequisites - -- [uv](https://docs.astral.sh/uv/#installation) installed. Any package manager including pip can also be used. -- [The Buf CLI](https://buf.build/docs/installation) installed, and include it in the `$PATH`. -- We'll also use [cURL](https://curl.se/). It's available from Homebrew and most Linux package managers. - -## Setup python environment - -First, we'll setup the python environment and dependencies. - -=== "ASGI" - - ```bash - uv init - uv add connect-python uvicorn - ``` - -=== "WSGI" - - ```bash - uv init - uv add connect-python gunicorn - ``` - -## Define a service - -Now we're ready to write the Protocol Buffer schema that defines our service. In your shell, - -```bash -mkdir -p proto/greet/v1 -touch proto/greet/v1/greet.proto -``` - -Open `proto/greet/v1/greet.proto` in your editor and add: - -```protobuf -syntax = "proto3"; - -package greet.v1; - -message GreetRequest { - string name = 1; -} - -message GreetResponse { - string greeting = 1; -} - -service GreetService { - rpc Greet(GreetRequest) returns (GreetResponse) {} -} -``` - -This file declares the `greet.v1` Protobuf package, a service called `GreetService`, and a single method -called `Greet` with its request and response structures. These package, service, and method names will -reappear soon in our HTTP API's URLs. - -## Generate code - -We're going to generate our code using [Buf](https://buf.build/), a modern replacement for Google's protobuf compiler. - -First, scaffold a basic [buf.yaml](https://buf.build/docs/configuration/v2/buf-yaml) by running `buf config init`. -Then, edit `buf.yaml` to use our `proto` directory: - -```yaml hl_lines="2 3" -version: v2 -modules: - - path: proto -lint: - use: - - DEFAULT -breaking: - use: - - FILE -``` - -We will use [remote plugins](https://buf.build/docs/bsr/remote-plugins/usage), a feature of the -[Buf Schema Registry](https://buf.build/docs/tutorials/getting-started-with-bsr) for generating code. Tell buf how to -generate code by creating a buf.gen.yaml: - -```bash -touch buf.gen.yaml -``` - -```yaml -version: v2 -plugins: - - remote: buf.build/protocolbuffers/python - out: . - - remote: buf.build/protocolbuffers/pyi - out: . - - remote: buf.build/connectrpc/python - out: . -``` - -With those configuration files in place, you can lint your schema and generate code: - -```bash -buf lint -buf generate -``` - -In the `greet` package, you should now see some generated Python: - -``` -greet - └── v1 - ├── greet_connect.py - └── greet_pb2.py - └── greet_pb2.pyi -``` - -The package `greet/v1` contains `greet_pb2.py` and `greet_pb2.pyi` which were generated by -the [protocolbuffers/python](https://buf.build/protocolbuffers/python) and -[protocolbuffers/pyi](https://buf.build/protocolbuffers/pyi) and contain `GreetRequest` -and `GreetResponse` structs and the associated marshaling code. `greet_connect.py` was -generated by [connectrpc/python](https://buf.build/connectrpc/python) and contains the -WSGI and ASGI service interfaces and client code to access a Connect server. Feel free to -poke around if you're interested - `greet_connect.py` is standard Python code. - -## Implement service - -The code we've generated takes care of the boring boilerplate, but we still need to implement our greeting logic. -In the generated code, this is represented as the `greet_connect.GreetService` and `greet_connect.GreetServiceSync` -interfaces for async ASGI and sync WSGI servers respectively. Since the interface is so small, we can do everything -in one Python file. `touch server.py` and add: - -=== "ASGI" - - ```python - from greet.v1.greet_connect import GreetService, GreetServiceASGIApplication - from greet.v1.greet_pb2 import GreetResponse - - class Greeter(GreetService): - async def greet(self, request, ctx): - print("Request headers: ", ctx.request_headers()) - response = GreetResponse(greeting=f"Hello, {request.name}!") - ctx.response_headers()["greet-version"] = "v1" - return response - - app = GreetServiceASGIApplication(Greeter()) - ``` - -=== "WSGI" - - ```python - from greet.v1.greet_connect import GreetServiceSync, GreetServiceWSGIApplication - from greet.v1.greet_pb2 import GreetResponse - - class Greeter(GreetServiceSync): - def greet(self, request, ctx): - print("Request headers: ", ctx.request_headers()) - response = GreetResponse(greeting=f"Hello, {request.name}!") - ctx.response_headers()["greet-version"] = "v1" - return response - - app = GreetServiceWSGIApplication(Greeter()) - ``` - -In a separate terminal window, you can now start your server: - -=== "ASGI" - - ```bash - uv run uvicorn server:app - ``` - -=== "WSGI" - - ```bash - uv run gunicorn server:app - ``` - -## Make requests - -The simplest way to consume your new API is an HTTP/1.1 POST with a JSON payload. If you have a recent version of -cURL installed, it's a one-liner: - -```bash -curl \ - --header "Content-Type: application/json" \ - --data '{"name": "Jane"}' \ - http://localhost:8000/greet.v1.GreetService/Greet -``` - -This responds: - -```json -{ - "greeting": "Hello, Jane!" -} -``` - -We can also make requests using Connect's generated client. `touch client.py` and add: - -=== "Async" - - ```python - import asyncio - - from greet.v1.greet_connect import GreetServiceClient - from greet.v1.greet_pb2 import GreetRequest - - async def main(): - client = GreetServiceClient("http://localhost:8000") - res = await client.greet(GreetRequest(name="Jane")) - print(res.greeting) - - if __name__ == "__main__": - asyncio.run(main()) - ``` - -=== "Sync" - - ```python - from greet.v1.greet_connect import GreetServiceClientSync - from greet.v1.greet_pb2 import GreetRequest - - def main(): - client = GreetServiceClientSync("http://localhost:8000") - res = client.greet(GreetRequest(name="Jane")) - print(res.greeting) - - if __name__ == "__main__": - main() - ``` - -With your server still running in a separate terminal window, you can now run your client: - -```bash -uv run python client.py -``` - -Congratulations — you've built your first Connect service! 🎉 - -## So what? - -With just a few lines of hand-written code, you've built a real API server that supports both the and Connect protocol. -Unlike a hand-written REST service, you didn't need to design a URL hierarchy, hand-write request and response structs, -manage your own marshaling, or parse typed values out of query parameters. More importantly, your users got an idiomatic, -type-safe client without any extra work on your part. - -## Next Steps - -- Learn about [Usage](./usage.md) patterns -- Explore the [API Reference](./api.md) -- Check out [Examples](examples/index.md) diff --git a/docs/grpc-compatibility.md b/docs/grpc-compatibility.md deleted file mode 100644 index 55b2e93..0000000 --- a/docs/grpc-compatibility.md +++ /dev/null @@ -1,98 +0,0 @@ -# gRPC compatibility - -Connect-Python currently does not support the gRPC protocol due to lack of support for HTTP/2 trailers -in the Python ecosystem. If you have an existing codebase using grpc-python and want to introduce Connect -in a transition without downtime, you will need a way for the gRPC servers to be accessible from both -gRPC and Connect clients at the same time. Envoy is a widely used proxy server with support for translating -the Connect protocol to gRPC via the [Connect-gRPC Bridge](https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/connect_grpc_bridge_filter). - -For example, if you have a gRPC server currently listening on port 8080, you update it to use port 8081 -and expose the service for both Connect and gRPC clients on port 8080 with this config. - -```yaml -admin: - address: - socket_address: { address: 0.0.0.0, port_value: 9090 } - -static_resources: - listeners: - - name: listener_0 - address: - socket_address: { address: 0.0.0.0, port_value: 8080 } - filter_chains: - - filters: - - name: envoy.filters.network.http_connection_manager - typed_config: - "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager - stat_prefix: ingress_http - codec_type: AUTO - route_config: - name: local_route - virtual_hosts: - - name: local_service - domains: ["*"] - routes: - - match: { prefix: "/" } - route: { cluster: service_0 } - http_filters: - - name: envoy.filters.http.connect_grpc_bridge - typed_config: - "@type": type.googleapis.com/envoy.extensions.filters.http.connect_grpc_bridge.v3.FilterConfig - - name: envoy.filters.http.grpc_web - typed_config: - "@type": type.googleapis.com/envoy.extensions.filters.http.grpc_web.v3.GrpcWeb - - name: envoy.filters.http.router - typed_config: - "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router - clusters: - - name: service_0 - connect_timeout: 0.25s - type: STRICT_DNS - lb_policy: ROUND_ROBIN - load_assignment: - cluster_name: service_0 - endpoints: - - lb_endpoints: - - endpoint: - address: - socket_address: - address: 127.0.0.1 - port_value: 8081 - typed_extension_protocol_options: - envoy.extensions.upstreams.http.v3.HttpProtocolOptions: - "@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions - explicit_http_config: - http2_protocol_options: - max_concurrent_streams: 100 -``` - -Refer to [Envoy docs](https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/security/ssl) for more configuration such -as TLS. - -## Migration - -Migrating from grpc-python to Connect using Envoy largely involves first adding Envoy in front of the server, -then migrating clients to Connect, and finally migrating servers to Connect and removing Envoy. - -The general code structure of grpc-python and Connect are very similar - if your code is configured to use a -type checker, any changes to parameter names and such should be quite easy to spot. - -1. Reconfigure your gRPC servers to include Envoy in front of the server port with a config similar to above. - For cloud deployments, this often means using functionality for sidecar containers. - -1. Begin generating code with `protoc-gen-connect-python`. - -1. Migrate clients to use Connect. Replace any special configuration of `ManagedChannel` with configured `httpx.Client` or - `httpx.AsyncClient` and switch to Connect's generated client types. If passing `metadata` at the call site, change - to `headers` - lists of string tuples can be passed directly to a `Headers` constructor, or can be changed to a raw - dictionary. Update any error handling to catch `ConnectError`. - -1. Complete deployment of all servers using Connect client. After this is done, your gRPC servers will only - be receiving traffic using the Connect protocol. - -1. Migrate service implementations to Connect generated stubs. It is recommended to extend the protocol classes - to have type checking find differences in method names. Change uses of `abort` to directly `raise ConnectError` - - for Connect services, it will be uncommon to pass the `RequestContext` into business logic code. - -1. Reconfigure server deployment to remove the Envoy proxy and deploy. You're done! You can stop generating code with - gRPC. diff --git a/docs/headers-and-trailers.md b/docs/headers-and-trailers.md deleted file mode 100644 index b61f4eb..0000000 --- a/docs/headers-and-trailers.md +++ /dev/null @@ -1,102 +0,0 @@ -# Headers & trailers - -To integrate with other systems, you may need to read or write custom HTTP headers with your RPCs. -For example, distributed tracing, authentication, authorization, and rate limiting often require -working with headers. Connect also supports trailers, which serve a similar purpose but can be written -after the response body. This document outlines how to work with headers and trailers. - -## Headers - -Connect headers are just HTTP headers - because Python's standard library does not provide a -corresponding type, we provide `request.Headers`. For most use cases, it is equivalent to a -dictionary while also providing additional methods to access multiple values for the same header -key when needed. Clients always accept a normal dictionary as well when accepting headers. - -In services, headers are available on the `RequestContext`: - -=== "ASGI" - - ```python - class GreetService: - async def greet(self, request, ctx): - print(ctx.request_headers().get("acme-tenant-id")) - ctx.response_headers()["greet-version"] = "v1" - return GreetResponse() - ``` - -=== "WSGI" - - ```python - class GreetService: - def greet(self, request, ctx): - print(ctx.request_headers().get("acme-tenant-id")) - ctx.response_headers()["greet-version"] = "v1" - return GreetResponse() - ``` - -For clients, we find that it is not common to read headers, but is fully supported. -To preserve client methods having simple signatures accepting and providing RPC -messages, headers are accessible through a separate context manager, `client.ResponseMetadata`. - -=== "Async" - - ```python - from connectrpc.client import ResponseMetadata - - client = GreetServiceClient("https://api.acme.com") - with ResponseMetadata() as meta: - res = await client.greet(GreetRequest(), headers={"acme-tenant-id": "1234"}) - print(meta.headers().get("greet-version")) - ``` - -=== "Sync" - - ```python - from connectrpc.client import ResponseMetadata - - client = GreetServiceClientSync("https://api.acme.com") - with ResponseMetadata() as meta: - res = client.greet(GreetRequest(), headers={"acme-tenant-id": "1234"}) - print(meta.headers().get("greet-version")) - ``` - -Supported protocols require that header keys contain only ASCII letters, numbers, underscores, hyphens, and -periods, and the protocols reserve all keys beginning with "Connect-" or "Grpc-". Similarly, header values may -contain only printable ASCII and spaces. In our experience, application code writing reserved or non-ASCII headers -is unusual; rather than wrapping `request.Headers` in a fat validation layer, we rely on your good judgment. - -## Trailers - -Connect's APIs for manipulating response trailers work identically to headers. Trailers are most useful in -streaming handlers, which may need to send some metadata to the client after sending a few messages. -Unary handlers should nearly always use headers instead. - -If you find yourself needing trailers, handlers and clients can access them much like headers: - -=== "Async" - - ```python - class GreetService: - async def greet(self, request, ctx): - ctx.response_trailers()["greet-version"] = "v1" - return GreetResponse() - - client = GreetServiceClient("https://api.acme.com") - with ResponseMetadata() as meta: - res = await client.greet(GreetRequest(), headers={"acme-tenant-id": "1234"}) - print(meta.trailers().get("greet-version")) - ``` - -=== "Sync" - - ```python - class GreetService: - def greet(self, request, ctx): - ctx.response_trailers()["greet-version"] = "v1" - return GreetResponse() - - client = GreetServiceClientSync("https://api.acme.com") - with ResponseMetadata() as meta: - res = client.greet(GreetRequest(), headers={"acme-tenant-id": "1234"}) - print(meta.trailers().get("greet-version")) - ``` diff --git a/docs/index.md b/docs/index.md deleted file mode 100644 index 1becde6..0000000 --- a/docs/index.md +++ /dev/null @@ -1,56 +0,0 @@ -# connect-python - -A Python implementation of the Connect RPC framework. - -This provides a client and server runtime for both synchronous and -asynchronous Python applications, as well as a stub code generator, -to let Python programs use the Connect protocol. - -## Features - -- Client backed by [httpx](https://www.python-httpx.org/) -- WSGI and ASGI server implementations for use with any Python app server -- Fully type-annotated, including the generated code, and verified - with pyright. -- Verified implementation using the official - [conformance](https://github.com/connectprc/conformance) test - suite. - -## Installation - -For basic client functionality: - -```bash -uv add connect-python -# Or pip install connect-python -``` - -For code generation, you will need the protoc plugin. This should generally -only be needed as a dev dependency. - -```bash -uv add --dev protoc-gen-connect-python -# Or pip install protoc-gen-connect-python -``` - -## Quick Start - -With a protobuf definition in hand, you can generate stub code. This is -easiest using buf, but you can also use protoc if you're feeling -masochistic. - -Install the compiler (eg `pip install protoc-gen-connect-python`), and -it can be referenced as `protoc-gen-connect-python`. - -A reasonable `buf.gen.yaml`: - -```yaml -version: v2 -plugins: - - remote: buf.build/protocolbuffers/python - out: . - - remote: buf.build/protocolbuffers/pyi - out: . - - local: .venv/bin/protoc-gen-connect-python - out: . -``` diff --git a/docs/interceptors.md b/docs/interceptors.md deleted file mode 100644 index af42daf..0000000 --- a/docs/interceptors.md +++ /dev/null @@ -1,265 +0,0 @@ -# Interceptors - -Interceptors are similar to the middleware or decorators you may be familiar with from other frameworks: -they're the primary way of extending Connect. They can modify the context, the request, the response, -and any errors. Interceptors are often used to add logging, metrics, tracing, retries, and other functionality. - -Take care when writing interceptors! They're powerful, but overly complex interceptors can make debugging difficult. - -## Interceptors are protocol implementations - -Connect interceptors are protocol implementations with the same signature as an RPC handler, along with a -call_next `Callable` to continue with request processing. This allows writing interceptors in much the same -way as any handler, making sure to call `call_next` when needing to call business logic - or not, if overriding -the response within the interceptor itself. - -Connect supports unary RPC and three stream types - because each has a different handler signature, we -provide protocols corresponding to each. - -=== "ASGI" - - ```python - class UnaryInterceptor(Protocol): - - async def intercept_unary( - self, - call_next: Callable[[REQ, RequestContext], Awaitable[RES]], - request: REQ, - ctx: RequestContext, - ) -> RES: ... - - class ClientStreamInterceptor(Protocol): - - async def intercept_client_stream( - self, - call_next: Callable[[AsyncIterator[REQ], RequestContext], Awaitable[RES]], - request: AsyncIterator[REQ], - ctx: RequestContext, - ) -> RES: ... - - class ServerStreamInterceptor(Protocol): - - def intercept_server_stream( - self, - call_next: Callable[[REQ, RequestContext], AsyncIterator[RES]], - request: REQ, - ctx: RequestContext, - ) -> AsyncIterator[RES]: ... - - class BidiStreamInterceptor(Protocol): - - def intercept_bidi_stream( - self, - call_next: Callable[[AsyncIterator[REQ], RequestContext], AsyncIterator[RES]], - request: AsyncIterator[REQ], - ctx: RequestContext, - ) -> AsyncIterator[RES]: ... - ``` - -=== "WSGI" - - ```python - class UnaryInterceptorSync(Protocol): - - def intercept_unary_sync( - self, - call_next: Callable[[REQ, RequestContext], RES], - request: REQ, - ctx: RequestContext, - ) -> RES: - - class ClientStreamInterceptorSync(Protocol): - - def intercept_client_stream_sync( - self, - call_next: Callable[[Iterator[REQ], RequestContext], RES], - request: Iterator[REQ], - ctx: RequestContext, - ) -> RES: - - class ServerStreamInterceptorSync(Protocol): - - def intercept_server_stream_sync( - self, - call_next: Callable[[REQ, RequestContext], Iterator[RES]], - request: REQ, - ctx: RequestContext, - ) -> Iterator[RES]: - - class BidiStreamInterceptorSync(Protocol): - - def intercept_bidi_stream_sync( - self, - call_next: Callable[[Iterator[REQ], RequestContext], Iterator[RES]], - request: Iterator[REQ], - ctx: RequestContext, - ) -> Iterator[RES]: - ``` - -A single class can implement as many of the protocols as needed. - -## An example - -That's a little abstract, so let's consider an example: we'd like to apply a filter to our greeting -service from the [getting started documentation](./getting-started.md) that says goodbye instead of -hello to certain callers. - -=== "ASGI" - - ```python - class GoodbyeInterceptor: - def __init__(self, users: list[str]): - self._users = users - - async def intercept_unary( - self, - call_next: Callable[[GreetRequest, RequestContext], Awaitable[GreetResponse]], - request: GreetRequest, - ctx: RequestContext, - ) -> GreetResponse: - if request.name in self._users: - return GreetResponse(greeting=f"Goodbye, {request.name}!") - return await call_next(request, ctx) - ``` - -=== "WSGI" - - ```python - class GoodbyeInterceptor: - def __init__(self, users: list[str]): - self._users = users - - def intercept_unary_sync( - self, - call_next: Callable[[GreetRequest, RequestContext], GreetResponse], - request: GreetRequest, - ctx: RequestContext, - ) -> GreetResponse: - if request.name in self._users: - return GreetResponse(greeting=f"Goodbye, {request.name}!") - return call_next(request, ctx) - ``` - -To apply our new interceptor to handlers, we can pass it to the application with `interceptors=`. - -=== "ASGI" - - ```python - app = GreetingServiceASGIApplication(service, interceptors=[GoodbyeInterceptor(["user1", "user2"])]) - ``` - -=== "WSGI" - - ```python - app = GreetingServiceWSGIApplication(service, interceptors=[GoodbyeInterceptor(["user1", "user2"])]) - ``` - -Client constructors also accept an `interceptors=` parameter. - -=== "Async" - - ```python - client = GreetingServiceClient("http://localhost:8000", interceptors=[GoodbyeInterceptor(["user1", "user2"])]) - ``` - -=== "Sync" - - ```python - client = GreetingServiceClientSync("http://localhost:8000", interceptors=[GoodbyeInterceptor(["user1", "user2"])]) - ``` - -## Metadata interceptors - -Because the signature is different for each RPC type, we have an interceptor protocol for each -to be able to intercept RPC messages. However, many interceptors, such as for authentication or -tracing, only need access to headers and not messages. Connect provides a metadata interceptor -protocol that can be implemented to work with any RPC type. - -An authentication interceptor checking bearer tokens and storing them to a context variable may look like this: - -=== "ASGI" - - ```python - from contextvars import ContextVar, Token - - _auth_token = ContextVar["auth_token"]("current_auth_token") - - class ServerAuthInterceptor: - def __init__(self, valid_tokens: list[str]): - self._valid_tokens = valid_tokens - - async def on_start(self, ctx: RequestContext) -> Token["auth_token"]: - authorization = ctx.request_headers().get("authorization") - if not authorization or not authorization.startswith("Bearer "): - raise ConnectError(Code.UNAUTHENTICATED) - token = authorization[len("Bearer "):] - if token not in self._valid_tokens: - raise ConnectError(Code.PERMISSION_DENIED) - return _auth_token.set(token) - - async def on_end(self, token: Token["auth_token"], ctx: RequestContext): - _auth_token.reset(token) - ``` - -=== "WSGI" - - ```python - from contextvars import ContextVar, Token - - _auth_token = ContextVar["auth_token"]("current_auth_token") - - class ServerAuthInterceptor: - def __init__(self, valid_tokens: list[str]): - self._valid_tokens = valid_tokens - - def on_start_sync(self, ctx: RequestContext) -> Token["auth_token"]: - authorization = ctx.request_headers().get("authorization") - if not authorization or not authorization.startswith("Bearer "): - raise ConnectError(Code.UNAUTHENTICATED) - token = authorization[len("Bearer "):] - if token not in self._valid_tokens: - raise ConnectError(Code.PERMISSION_DENIED) - return _auth_token.set(token) - - def on_end_sync(self, token: Token["auth_token"], ctx: RequestContext): - _auth_token.reset(token) - ``` - -`on_start` can return any value, which is passed to the optional `on_end` method. Here, we -return the token to reset the context variable. - -Clients can add an interceptor that reads the token from the context variable and populates -the authorization header. - -=== "Async" - - ```python - from contextvars import ContextVar - - _auth_token = ContextVar["auth_token"]("current_auth_token") - - class ClientAuthInterceptor: - async def on_start(self, ctx: RequestContext): - auth_token = _auth_token.get(None) - if auth_token: - ctx.request_headers()["authorization"] = f"Bearer {auth_token}" - ``` - -=== "Sync" - - ```python - from contextvars import ContextVar - - _auth_token = ContextVar["auth_token"]("current_auth_token") - - class ClientAuthInterceptor: - def on_start_sync(self, ctx: RequestContext): - auth_token = _auth_token.get(None) - if auth_token: - ctx.request_headers()["authorization"] = f"Bearer {auth_token}" - ``` - -Note that in the client interceptor, we do not need to define `on_end`. - -The above interceptors would allow a server to receive and validate an auth token and automatically -propagate it to the authorization header of backend calls. diff --git a/docs/streaming.md b/docs/streaming.md deleted file mode 100644 index cc8b1e4..0000000 --- a/docs/streaming.md +++ /dev/null @@ -1,131 +0,0 @@ -# Streaming - -Connect supports several types of streaming RPCs. Streaming is exciting — it's fundamentally different from -the web's typical request-response model, and in the right circumstances it can be very efficient. If you've -been writing the same pagination or polling code for years, streaming may look like the answer to all your -problems. - -Temper your enthusiasm. Streaming also comes with many drawbacks: - -- It requires excellent HTTP libraries. At the very least, the client and server must be able to stream HTTP/1.1 - request and response bodies. For bidirectional streaming, both parties must support HTTP/2. Long-lived streams are - much more likely to encounter bugs and edge cases in HTTP/2 flow control. - -- It requires excellent proxies. Every proxy between the server and client — including those run by cloud providers — - must support HTTP/2. - -- It weakens the protections offered to your unary handlers, since streaming typically requires proxies to be - configured with much longer timeouts. - -- It requires complex tools. Streaming RPC protocols are much more involved than unary protocols, so cURL and your - browser's network inspector are useless. - -In general, streaming ties your application more closely to your networking infrastructure and makes your application -inaccessible to less-sophisticated clients. You can minimize these downsides by keeping streams short-lived. - -All that said, `connect-python` fully supports client and server streaming. Bidirectional streaming is currently not -supported for clients and requires an HTTP/2 ASGI server for servers. - -## Streaming variants - -In Python, streaming messages use standard `AsyncIterator` for async servers and clients, or `Iterator` for sync servers -and clients. - -In _client streaming_, the client sends multiple messages. Once the server receives all the messages, it responds with -a single message. In Protobuf schemas, client streaming methods look like this: - -```protobuf -service GreetService { - rpc Greet(stream GreetRequest) returns (GreetResponse) {} -} -``` - -In _server streaming_, the client sends a single message and the server responds with multiple messages. In Protobuf -schemas, server streaming methods look like this: - -```protobuf -service GreetService { - rpc Greet(GreetRequest) returns (stream GreetResponse) {} -} -``` - -In _bidirectional streaming_ (often called bidi), the client and server may both send multiple messages. Often, the -exchange is structured like a conversation: the client sends a message, the server responds, the client sends another -message, and so on. Keep in mind that this always requires end-to-end HTTP/2 support! - -## HTTP representation - -Streaming responses always have an HTTP status of 200 OK. This may seem unusual, but it's unavoidable: the server may -encounter an error after sending a few messages, when the HTTP status has already been sent to the client. Rather than -relying on the HTTP status, streaming handlers encode any errors in HTTP trailers or at the end of the response body. - -The body of streaming requests and responses envelopes your schema-defined messages with a few bytes of -protocol-specific binary framing data. Because of the interspersed framing data, the payloads are no longer valid -Protobuf or JSON: instead, they use protocol-specific Content-Types like `application/connect+proto`. - -## An example - -Let's start by amending the `GreetService` we defined in [Getting Started](./getting-started.md) to make the Greet method use -client streaming: - -```protobuf -syntax = "proto3"; - -package greet.v1; - -message GreetRequest { - string name = 1; -} - -message GreetResponse { - string greeting = 1; -} - -service GreetService { - rpc Greet(stream GreetRequest) returns (GreetResponse) {} -} -``` - -After running `buf generate` to update our generated code, we can amend our service implementation in -`server.py`: - -=== "ASGI" - - ```python - from greet.v1.greet_connect import GreetService, GreetServiceASGIApplication - from greet.v1.greet_pb2 import GreetResponse - - class Greeter(GreetService): - async def greet(self, request, ctx): - print("Request headers: ", ctx.request_headers()) - greeting = "" - async for message in request: - greeting += f"Hello, {message.name}!\n" - response = GreetResponse(greeting=greeting) - ctx.response_headers()["greet-version"] = "v1" - return response - - app = GreetServiceASGIApplication(Greeter()) - ``` - -=== "WSGI" - - ```python - from greet.v1.greet_connect import GreetServiceSync, GreetServiceWSGIApplication - from greet.v1.greet_pb2 import GreetResponse - - class Greeter(GreetServiceSync): - def greet(self, request, ctx): - print("Request headers: ", ctx.request_headers()) - greeting = "" - for message in request: - greeting += f"Hello, {message.name}!\n" - response = GreetResponse(greeting=f"Hello, {request.name}!") - ctx.response_headers()["greet-version"] = "v1" - return response - - app = GreetServiceWSGIApplication(Greeter()) - ``` - -That's it - metadata interceptors such as our [simple authentication interceptor](./interceptors.md#metadata-interceptors) -can be used as-is with no other changes. diff --git a/docs/testing.md b/docs/testing.md deleted file mode 100644 index 8d5d4b8..0000000 --- a/docs/testing.md +++ /dev/null @@ -1,453 +0,0 @@ -# Testing - -This guide covers testing connect-python services and clients. - -## Setup - -For pytest examples in this guide, you'll need pytest and pytest-asyncio. unittest requires no additional dependencies. - -## Recommended approach: In-memory testing - -The recommended approach is **in-memory testing** using httpx's ASGI/WSGI transports (provided by httpx, not connect-python). This tests your full application stack (routing, serialization, error handling, interceptors) while remaining fast and isolated - no network overhead or port conflicts. - -Here's a minimal example without any test framework: - -=== "ASGI" - - ```python - import httpx - from greet.v1.greet_connect import GreetServiceASGIApplication, GreetServiceClient - from greet.v1.greet_pb2 import GreetRequest - from server import Greeter # Your service implementation - - # Create ASGI app with your service - app = GreetServiceASGIApplication(Greeter()) - - # Connect client to service using in-memory transport - async with httpx.AsyncClient( - transport=httpx.ASGITransport(app=app), - base_url="http://test" # URL is ignored for in-memory transport - ) as session: - client = GreetServiceClient("http://test", session=session) - response = await client.greet(GreetRequest(name="Alice")) - - print(response.greeting) # "Hello, Alice!" - ``` - -=== "WSGI" - - ```python - import httpx - from greet.v1.greet_connect import GreetServiceWSGIApplication, GreetServiceClientSync - from greet.v1.greet_pb2 import GreetRequest - from server import GreeterSync # Your service implementation - - # Create WSGI app with your service - app = GreetServiceWSGIApplication(GreeterSync()) - - # Connect client to service using in-memory transport - with httpx.Client( - transport=httpx.WSGITransport(app=app), - base_url="http://test" # URL is ignored for in-memory transport - ) as session: - client = GreetServiceClientSync("http://test", session=session) - response = client.greet(GreetRequest(name="Alice")) - - print(response.greeting) # "Hello, Alice!" - ``` - -This pattern works with any test framework (pytest, unittest) or none at all. The examples below show how to integrate with both pytest and unittest. - -## Testing servers - -### Using pytest - -Testing the service we created in the [Getting Started](getting-started.md) guide looks like this: - -=== "ASGI" - - ```python - import httpx - import pytest - from greet.v1.greet_connect import GreetServiceASGIApplication, GreetServiceClient - from greet.v1.greet_pb2 import GreetRequest - from server import Greeter # Import your actual service implementation - - @pytest.mark.asyncio - async def test_greet(): - # Create the ASGI application with your service - app = GreetServiceASGIApplication(Greeter()) - - # Test using httpx with ASGI transport - async with httpx.AsyncClient( - transport=httpx.ASGITransport(app=app), - base_url="http://test" - ) as session: - client = GreetServiceClient("http://test", session=session) - response = await client.greet(GreetRequest(name="Alice")) - - assert response.greeting == "Hello, Alice!" - ``` - -=== "WSGI" - - ```python - import httpx - from greet.v1.greet_connect import GreetServiceWSGIApplication, GreetServiceClientSync - from greet.v1.greet_pb2 import GreetRequest - from server import GreeterSync # Import your actual service implementation - - def test_greet(): - # Create the WSGI application with your service - app = GreetServiceWSGIApplication(GreeterSync()) - - # Test using httpx with WSGI transport - with httpx.Client( - transport=httpx.WSGITransport(app=app), - base_url="http://test" - ) as session: - client = GreetServiceClientSync("http://test", session=session) - response = client.greet(GreetRequest(name="Alice")) - - assert response.greeting == "Hello, Alice!" - ``` - -### Using unittest - -The same in-memory testing approach works with unittest: - -=== "ASGI" - - ```python - import asyncio - import httpx - import unittest - from greet.v1.greet_connect import GreetServiceASGIApplication, GreetServiceClient - from greet.v1.greet_pb2 import GreetRequest - from server import Greeter - - class TestGreet(unittest.TestCase): - def test_greet(self): - async def run_test(): - app = GreetServiceASGIApplication(Greeter()) - async with httpx.AsyncClient( - transport=httpx.ASGITransport(app=app), - base_url="http://test" - ) as session: - client = GreetServiceClient("http://test", session=session) - response = await client.greet(GreetRequest(name="Alice")) - self.assertEqual(response.greeting, "Hello, Alice!") - - asyncio.run(run_test()) - ``` - -=== "WSGI" - - ```python - import httpx - import unittest - from greet.v1.greet_connect import GreetServiceWSGIApplication, GreetServiceClientSync - from greet.v1.greet_pb2 import GreetRequest - from server import GreeterSync - - class TestGreet(unittest.TestCase): - def test_greet(self): - app = GreetServiceWSGIApplication(GreeterSync()) - with httpx.Client( - transport=httpx.WSGITransport(app=app), - base_url="http://test" - ) as session: - client = GreetServiceClientSync("http://test", session=session) - response = client.greet(GreetRequest(name="Alice")) - self.assertEqual(response.greeting, "Hello, Alice!") - ``` - -This approach: - -- Tests your full application stack (routing, serialization, error handling) -- Runs fast without network overhead -- Provides isolation between tests -- Works with all streaming types - -For integration tests with actual servers over TCP/HTTP, see standard pytest patterns for [server fixtures](https://docs.pytest.org/en/stable/how-to/fixtures.html). - -### Using fixtures for reusable test setup - -For cleaner tests, use pytest fixtures to set up clients and services: - -=== "ASGI" - - ```python - import httpx - import pytest - import pytest_asyncio - from greet.v1.greet_connect import GreetServiceASGIApplication, GreetServiceClient - from greet.v1.greet_pb2 import GreetRequest - from server import Greeter - - @pytest_asyncio.fixture - async def greet_client(): - app = GreetServiceASGIApplication(Greeter()) - async with httpx.AsyncClient( - transport=httpx.ASGITransport(app=app), - base_url="http://test" - ) as session: - yield GreetServiceClient("http://test", session=session) - - @pytest.mark.asyncio - async def test_greet(greet_client): - response = await greet_client.greet(GreetRequest(name="Alice")) - assert response.greeting == "Hello, Alice!" - - @pytest.mark.asyncio - async def test_greet_empty_name(greet_client): - response = await greet_client.greet(GreetRequest(name="")) - assert response.greeting == "Hello, !" - ``` - -=== "WSGI" - - ```python - import httpx - import pytest - from greet.v1.greet_connect import GreetServiceWSGIApplication, GreetServiceClientSync - from greet.v1.greet_pb2 import GreetRequest - from server import GreeterSync - - @pytest.fixture - def greet_client(): - app = GreetServiceWSGIApplication(GreeterSync()) - with httpx.Client( - transport=httpx.WSGITransport(app=app), - base_url="http://test" - ) as session: - yield GreetServiceClientSync("http://test", session=session) - - def test_greet(greet_client): - response = greet_client.greet(GreetRequest(name="Alice")) - assert response.greeting == "Hello, Alice!" - - def test_greet_empty_name(greet_client): - response = greet_client.greet(GreetRequest(name="")) - assert response.greeting == "Hello, !" - ``` - -This pattern: - -- Reduces code duplication across multiple tests -- Makes tests more readable and focused on behavior -- Follows pytest best practices -- Matches the pattern used in connect-python's own test suite - -With your test client setup, you can use any connect code for interacting with the service under test including [streaming](streaming.md), reading [headers and trailers](headers-and-trailers.md), or checking [errors](errors.md). For example, to test error handling: - -```python -with pytest.raises(ConnectError) as exc_info: - await client.greet(GreetRequest(name="")) - -assert exc_info.value.code == Code.INVALID_ARGUMENT -``` - -See the [Errors](errors.md) guide for more details on error handling. - - -## Testing clients - -For testing client code that calls Connect services, use the same in-memory testing approach shown above. Create a test service implementation and use httpx transports to test your client logic without network overhead. - -### Example: Testing client error handling - -=== "Async" - - ```python - import pytest - import httpx - from connectrpc.code import Code - from connectrpc.errors import ConnectError - from greet.v1.greet_connect import GreetService, GreetServiceASGIApplication, GreetServiceClient - from greet.v1.greet_pb2 import GreetRequest, GreetResponse - - async def fetch_user_greeting(user_id: str, client: GreetServiceClient): - """Client code that handles errors.""" - try: - response = await client.greet(GreetRequest(name=user_id)) - return response.greeting - except ConnectError as e: - if e.code == Code.NOT_FOUND: - return "User not found" - elif e.code == Code.UNAUTHENTICATED: - return "Please login" - raise - - @pytest.mark.asyncio - async def test_client_error_handling(): - class TestGreetService(GreetService): - async def greet(self, request, ctx): - if request.name == "unknown": - raise ConnectError(Code.NOT_FOUND, "User not found") - return GreetResponse(greeting=f"Hello, {request.name}!") - - app = GreetServiceASGIApplication(TestGreetService()) - async with httpx.AsyncClient( - transport=httpx.ASGITransport(app=app), - base_url="http://test" - ) as session: - client = GreetServiceClient("http://test", session=session) - - # Test successful case - result = await fetch_user_greeting("Alice", client) - assert result == "Hello, Alice!" - - # Test error handling - result = await fetch_user_greeting("unknown", client) - assert result == "User not found" - ``` - -=== "Sync" - - ```python - import httpx - from connectrpc.code import Code - from connectrpc.errors import ConnectError - from greet.v1.greet_connect import GreetServiceSync, GreetServiceWSGIApplication, GreetServiceClientSync - from greet.v1.greet_pb2 import GreetRequest, GreetResponse - - def fetch_user_greeting(user_id: str, client: GreetServiceClientSync): - """Client code that handles errors.""" - try: - response = client.greet(GreetRequest(name=user_id)) - return response.greeting - except ConnectError as e: - if e.code == Code.NOT_FOUND: - return "User not found" - elif e.code == Code.UNAUTHENTICATED: - return "Please login" - raise - - def test_client_error_handling(): - class TestGreetServiceSync(GreetServiceSync): - def greet(self, request, ctx): - if request.name == "unknown": - raise ConnectError(Code.NOT_FOUND, "User not found") - return GreetResponse(greeting=f"Hello, {request.name}!") - - app = GreetServiceWSGIApplication(TestGreetServiceSync()) - with httpx.Client( - transport=httpx.WSGITransport(app=app), - base_url="http://test" - ) as session: - client = GreetServiceClientSync("http://test", session=session) - - # Test successful case - result = fetch_user_greeting("Alice", client) - assert result == "Hello, Alice!" - - # Test error handling - result = fetch_user_greeting("unknown", client) - assert result == "User not found" - ``` - -## Testing interceptors - -Test interceptors as part of your full application stack. For example, testing the `ServerAuthInterceptor` from the [Interceptors](interceptors.md#metadata-interceptors) guide: - -=== "ASGI" - - ```python - import httpx - import pytest - from connectrpc.code import Code - from connectrpc.errors import ConnectError - from greet.v1.greet_connect import GreetServiceASGIApplication, GreetServiceClient - from greet.v1.greet_pb2 import GreetRequest - from interceptors import ServerAuthInterceptor - from server import Greeter - - @pytest.mark.asyncio - async def test_server_auth_interceptor(): - interceptor = ServerAuthInterceptor(["valid-token"]) - app = GreetServiceASGIApplication( - Greeter(), - interceptors=[interceptor] - ) - - async with httpx.AsyncClient( - transport=httpx.ASGITransport(app=app), - base_url="http://test" - ) as session: - client = GreetServiceClient("http://test", session=session) - - # Valid token succeeds - response = await client.greet( - GreetRequest(name="Alice"), - headers={"authorization": "Bearer valid-token"} - ) - assert response.greeting == "Hello, Alice!" - - # Invalid token format fails with UNAUTHENTICATED - with pytest.raises(ConnectError) as exc_info: - await client.greet( - GreetRequest(name="Bob"), - headers={"authorization": "invalid"} - ) - assert exc_info.value.code == Code.UNAUTHENTICATED - - # Wrong token fails with PERMISSION_DENIED - with pytest.raises(ConnectError) as exc_info: - await client.greet( - GreetRequest(name="Bob"), - headers={"authorization": "Bearer wrong-token"} - ) - assert exc_info.value.code == Code.PERMISSION_DENIED - ``` - -=== "WSGI" - - ```python - import httpx - import pytest - from connectrpc.code import Code - from connectrpc.errors import ConnectError - from greet.v1.greet_connect import GreetServiceWSGIApplication, GreetServiceClientSync - from greet.v1.greet_pb2 import GreetRequest - from interceptors import ServerAuthInterceptor - from server import GreeterSync - - def test_server_auth_interceptor(): - interceptor = ServerAuthInterceptor(["valid-token"]) - app = GreetServiceWSGIApplication( - GreeterSync(), - interceptors=[interceptor] - ) - - with httpx.Client( - transport=httpx.WSGITransport(app=app), - base_url="http://test" - ) as session: - client = GreetServiceClientSync("http://test", session=session) - - # Valid token succeeds - response = client.greet( - GreetRequest(name="Alice"), - headers={"authorization": "Bearer valid-token"} - ) - assert response.greeting == "Hello, Alice!" - - # Invalid token format fails with UNAUTHENTICATED - with pytest.raises(ConnectError) as exc_info: - client.greet( - GreetRequest(name="Bob"), - headers={"authorization": "invalid"} - ) - assert exc_info.value.code == Code.UNAUTHENTICATED - - # Wrong token fails with PERMISSION_DENIED - with pytest.raises(ConnectError) as exc_info: - client.greet( - GreetRequest(name="Bob"), - headers={"authorization": "Bearer wrong-token"} - ) - assert exc_info.value.code == Code.PERMISSION_DENIED - ``` - -See the [Interceptors](interceptors.md) guide for more details on implementing interceptors. diff --git a/docs/usage.md b/docs/usage.md deleted file mode 100644 index 969c3a6..0000000 --- a/docs/usage.md +++ /dev/null @@ -1,310 +0,0 @@ -# Usage Guide - -## Basic Client Usage - -### Asynchronous Client - -```python -from your_generated_code.eliza_connect import ElizaServiceClient -from your_generated_code import eliza_pb2 - -async def main(): - async with ElizaServiceClient("https://demo.connectrpc.com") as eliza_client: - # Unary responses: await and get the response message back - response = await eliza_client.say(eliza_pb2.SayRequest(sentence="Hello, Eliza!")) - print(f" Eliza says: {response.sentence}") - - # Streaming responses: use async for to iterate over messages in the stream - req = eliza_pb2.IntroduceRequest(name="Henry") - async for response in eliza_client.introduce(req): - print(f" Eliza: {response.sentence}") - - # Streaming requests: send an iterator, get a single message - async def pontificate_requests(): - yield eliza_pb2.PontificateRequest(sentence="I have many things on my mind.") - yield eliza_pb2.PontificateRequest(sentence="But I will save them for later.") - response = await eliza_client.pontificate(pontificate_requests()) - print(f" Eliza responds: {response.sentence}") - - # Bidirectional RPCs: send an iterator, get an iterator - async def converse_requests(): - yield eliza_pb2.ConverseRequest(sentence="I have been having trouble communicating.") - yield eliza_pb2.ConverseRequest(sentence="But structured RPCs are pretty great!") - yield eliza_pb2.ConverseRequest(sentence="What do you think?") - async for response in eliza_client.converse(converse_requests()): - print(f" Eliza: {response.sentence}") -``` - -### Synchronous Client - -```python -from your_generated_code.eliza_connect import ElizaServiceClientSync -from your_generated_code import eliza_pb2 - -# Create client -eliza_client = ElizaServiceClientSync("https://demo.connectrpc.com") - -# Unary responses: -response = eliza_client.say(eliza_pb2.SayRequest(sentence="Hello, Eliza!")) -print(f" Eliza says: {response.sentence}") - -# Streaming responses: use 'for' to iterate over messages in the stream -req = eliza_pb2.IntroduceRequest(name="Henry") -for response in eliza_client.introduce(req): - print(f" Eliza: {response.sentence}") - -# Streaming requests: send an iterator, get a single message -requests = [ - eliza_pb2.PontificateRequest(sentence="I have many things on my mind."), - eliza_pb2.PontificateRequest(sentence="But I will save them for later."), -] -response = eliza_client.pontificate(requests) -print(f" Eliza responds: {response.sentence}") - -# Bidirectional RPCs: send an iterator, get an iterator. -requests = [ - eliza_pb2.ConverseRequest(sentence="I have been having trouble communicating."), - eliza_pb2.ConverseRequest(sentence="But structured RPCs are pretty great!"), - eliza_pb2.ConverseRequest(sentence="What do you think?") -] -for response in eliza_client.converse(requests): - print(f" Eliza: {response.sentence}") -``` - -## Advanced Usage - -### Sending Extra Headers - -All RPC methods take an `headers` argument; you can use a `dict[str, str]` or -a `Headers` object if needing to send multiple values for a key. - -```python -eliza_client.say(req, headers={"X-Favorite-RPC": "Connect"}) -``` - -### Per-request Timeouts - -All RPC methods take a `timeout_ms: int` argument: - -```python -eliza_client.say(req, timeout_ms=250) -``` - -The timeout will be used in two ways: - -1. It will be set in the `Connect-Timeout-Ms` header, so the server will be informed of the deadline -2. The HTTP client will be informed, and will close the request if the timeout expires -3. For asynchronous clients, the RPC invocation itself will be timed-out without relying on the I/O stack - -### Response Metadata - -For access to response headers or trailers, wrap invocations with the `ResponseMetadata` context manager. - -```python -with ResponseMetadata() as meta: - response = eliza_client.say(req) - print(response.sentence) - print(meta.headers()) - print(meta.trailers()) -``` - -## Server Implementation - -### ASGI Server - -The generated code includes a class to mount an object implementing your service as a ASGI application: - -```python -class ElizaServiceASGIApplication(service: ElizaService): - ... -``` - -Your implementation needs to follow the `ElizaService` protocol: - -```python -from typing import AsyncIterator -from connectrpc.request import RequestContext -from your_generated_code import eliza_pb2 - -class ElizaServiceImpl: - async def say(self, request: eliza_pb2.SayRequest, ctx: RequestContext) -> eliza_pb2.SayResponse: - return eliza_pb2.SayResponse(sentence=f"You said: {request.sentence}") - - async def converse(self, request: AsyncIterator[eliza_pb2.ConverseRequest], ctx: RequestContext) -> AsyncIterator[eliza_pb2.ConverseResponse]: - async for message in request: - yield eliza_pb2.ConverseResponse(sentence=f"You said: {message.sentence}") -``` - -### WSGI Server - -The generated code includes a class to mount an object implementing your service as a WSGI application: - -```python -class ElizaServiceWSGIApplication(service: ElizaServiceSync): - ... -``` - -Your implementation needs to follow the `ElizaServiceSync` protocol: - -```python -from typing import Iterator -from connectrpc.request import RequestContext -from your_generated_code import eliza_pb2 - -class ElizaServiceImpl: - def say(self, request: eliza_pb2.SayRequest, ctx: RequestContext) -> eliza_pb2.SayResponse: - return eliza_pb2.SayResponse(sentence=f"You said: {request.sentence}") - - def converse(self, request: Iterator[eliza_pb2.ConverseRequest], ctx: RequestContext) -> Iterator[eliza_pb2.ConverseResponse]: - for message in request: - yield eliza_pb2.ConverseResponse(sentence=f"You said: {message.sentence}") -``` - -## Error Handling Best Practices - -### Choosing appropriate error codes - -Select error codes that accurately reflect the situation: - -- Use `INVALID_ARGUMENT` for malformed requests that should never be retried -- Use `FAILED_PRECONDITION` for requests that might succeed if the system state changes -- Use `UNAVAILABLE` for transient failures that should be retried -- Use `INTERNAL` sparingly - it indicates a bug in your code - -For more detailed guidance on choosing error codes, see the [Connect protocol documentation](https://connectrpc.com/docs/protocol#error-codes). - -### Providing helpful error messages - -Error messages should help the caller understand what went wrong and how to fix it: - -```python -# Good - specific and actionable -raise ConnectError(Code.INVALID_ARGUMENT, "email must contain an @ symbol") - -# Less helpful - too vague -raise ConnectError(Code.INVALID_ARGUMENT, "invalid input") -``` - -### Using error details for structured data - -Rather than encoding structured information in error messages, use typed error details. For example: - -```python -from google.rpc.error_details_pb2 import BadRequest - -# Good - structured details -bad_request = BadRequest() -for field, error in validation_errors.items(): - violation = bad_request.field_violations.add() - violation.field = field - violation.description = error -raise ConnectError(Code.INVALID_ARGUMENT, "Validation failed", details=[bad_request]) - -# Less structured - information in message -raise ConnectError( - Code.INVALID_ARGUMENT, - f"Validation failed: email: {email_error}, name: {name_error}" -) -``` - -**Note**: While error details provide structured error information, they require client-side deserialization to be fully useful for debugging. Make sure to document expected error detail types in your API documentation to help consumers properly handle them. - -### Security considerations - -Avoid including sensitive data in error messages or details that will be sent to clients. For example: - -```python -# Bad - leaks internal details -raise ConnectError(Code.INTERNAL, f"Database query failed: {sql_query}") - -# Good - generic message -raise ConnectError(Code.INTERNAL, "Failed to complete request") -``` - -### Handling timeouts - -Client timeouts are represented with `Code.DEADLINE_EXCEEDED`: - -```python -from connectrpc.code import Code -from connectrpc.errors import ConnectError - -async with GreetServiceClient("http://localhost:8000") as client: - try: - response = await client.greet(GreetRequest(name="World"), timeout_ms=1000) - except ConnectError as e: - if e.code == Code.DEADLINE_EXCEEDED: - print("Operation timed out") -``` - -### Implementing retry logic - -Some errors are retriable. Use appropriate error codes to signal this. Here's an example implementation: - -```python -import asyncio -from connectrpc.code import Code -from connectrpc.errors import ConnectError - -async def call_with_retry(client, request, max_attempts=3): - """Retry logic for transient failures.""" - for attempt in range(max_attempts): - try: - return await client.greet(request) - except ConnectError as e: - # Only retry transient errors - if e.code == Code.UNAVAILABLE and attempt < max_attempts - 1: - await asyncio.sleep(2 ** attempt) # Exponential backoff - continue - raise -``` - -### Error transformation in interceptors - -Interceptors can catch and transform errors. This is useful for adding context, converting error types, or implementing retry logic. For example: - -=== "ASGI" - - ```python - from connectrpc.code import Code - from connectrpc.errors import ConnectError - - class ErrorLoggingInterceptor: - async def intercept_unary(self, call_next, request, ctx): - try: - return await call_next(request, ctx) - except ConnectError as e: - # Log the error with context - method = ctx.method() - print(f"Error in {method.service_name}/{method.name}: {e.code} - {e.message}") - # Re-raise the error - raise - except Exception as e: - # Convert unexpected errors to ConnectError - method = ctx.method() - print(f"Unexpected error in {method.service_name}/{method.name}: {e}") - raise ConnectError(Code.INTERNAL, "An unexpected error occurred") - ``` - -=== "WSGI" - - ```python - from connectrpc.code import Code - from connectrpc.errors import ConnectError - - class ErrorLoggingInterceptor: - def intercept_unary_sync(self, call_next, request, ctx): - try: - return call_next(request, ctx) - except ConnectError as e: - # Log the error with context - method = ctx.method() - print(f"Error in {method.service_name}/{method.name}: {e.code} - {e.message}") - # Re-raise the error - raise - except Exception as e: - # Convert unexpected errors to ConnectError - method = ctx.method() - print(f"Unexpected error in {method.service_name}/{method.name}: {e}") - raise ConnectError(Code.INTERNAL, "An unexpected error occurred") - ``` diff --git a/justfile b/justfile index 16e2dfe..9923fcc 100644 --- a/justfile +++ b/justfile @@ -20,53 +20,44 @@ typecheck: uv run pyright # Run unit tests with no extras -[working-directory: 'noextras'] +[working-directory('noextras')] test-noextras *args: - uv run --exact pytest {{args}} + uv run --exact pytest {{ args }} # Run unit tests test *args: (test-noextras args) - uv run pytest {{args}} + uv run pytest {{ args }} # Run lint, typecheck and test check: lint typecheck test # Run conformance tests -[working-directory: 'conformance'] +[working-directory('conformance')] conformance *args: - uv run pytest {{args}} - -# Build docs -docs: - uv run mkdocs build - -# Serve the docs locally -[working-directory: 'site'] -docs-serve: docs - uv run python -m http.server 8000 + uv run pytest {{ args }} # Generate gRPC status generate-status: - go run github.com/bufbuild/buf/cmd/buf@{{BUF_VERSION}} generate + go run github.com/bufbuild/buf/cmd/buf@{{ BUF_VERSION }} generate # Generate conformance files -[working-directory: 'conformance'] +[working-directory('conformance')] generate-conformance: - go run github.com/bufbuild/buf/cmd/buf@{{BUF_VERSION}} generate + go run github.com/bufbuild/buf/cmd/buf@{{ BUF_VERSION }} generate @# We use the published conformance protos for tests, but need to make sure their package doesn't start with connectrpc @# which conflicts with the runtime package. Since protoc python plugin does not provide a way to change the package @# structure, we use sed to fix the imports instead. LC_ALL=c find test/gen -type f -exec sed -i '' 's/from connectrpc.conformance.v1/from gen.connectrpc.conformance.v1/' {} + # Generate example files -[working-directory: 'example'] +[working-directory('example')] generate-example: - go run github.com/bufbuild/buf/cmd/buf@{{BUF_VERSION}} generate + go run github.com/bufbuild/buf/cmd/buf@{{ BUF_VERSION }} generate # Generate test files -[working-directory: 'test'] +[working-directory('test')] generate-test: - go run github.com/bufbuild/buf/cmd/buf@{{BUF_VERSION}} generate + go run github.com/bufbuild/buf/cmd/buf@{{ BUF_VERSION }} generate # Run all generation targets, and format the generated code generate: generate-conformance generate-example generate-status generate-test format @@ -76,6 +67,6 @@ checkgenerate: generate test -z "$(git status --porcelain | tee /dev/stderr)" bump *args: - uv run bump-my-version bump {{args}} + uv run bump-my-version bump {{ args }} uv lock cd protoc-gen-connect-python && uv lock diff --git a/pyproject.toml b/pyproject.toml index 067bf36..fd740b6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,69 +2,65 @@ name = "connect-python" version = "0.7.1" description = "Server and client runtime library for Connect RPC" -maintainers = [ - { name = "Anuraag Agrawal", email = "anuraaga@gmail.com" }, - { name = "Spencer Nelson", email = "spencer@firetiger.com" }, - { name = "Stefan VanBuren", email = "svanburen@buf.build" }, - { name = "Yasushi Itoh", email = "i2y.may.roku@gmail.com" }, -] -requires-python = ">= 3.10" -dependencies = ["httpx>=0.28.1", "protobuf>=5.28"] readme = "README.md" +requires-python = ">= 3.10" license-files = ["LICENSE"] -keywords = ["rpc", "grpc", "connect", "protobuf", "http"] +maintainers = [ + { name = "Anuraag Agrawal", email = "anuraaga@gmail.com" }, + { name = "Spencer Nelson", email = "spencer@firetiger.com" }, + { name = "Stefan VanBuren", email = "svanburen@buf.build" }, + { name = "Yasushi Itoh", email = "i2y.may.roku@gmail.com" }, +] +keywords = ["connect", "grpc", "http", "protobuf", "rpc"] classifiers = [ - "Development Status :: 3 - Alpha", - "Intended Audience :: Developers", - "License :: OSI Approved :: Apache Software License", - "Operating System :: OS Independent", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Programming Language :: Python :: 3.13", - "Programming Language :: Python :: 3.14", - "Topic :: Internet :: WWW/HTTP", - "Topic :: Software Development :: Libraries :: Python Modules", - "Typing :: Typed", + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "Topic :: Internet :: WWW/HTTP", + "Topic :: Software Development :: Libraries :: Python Modules", + "Typing :: Typed", ] +dependencies = ["httpx>=0.28.1", "protobuf>=5.28"] [project.urls] Homepage = "https://github.com/connectrpc/connect-python" -Repository = "https://github.com/connectrpc/connect-python" Issues = "https://github.com/connectrpc/connect-python/issues" +Repository = "https://github.com/connectrpc/connect-python" [dependency-groups] dev = [ - "asgiref==3.9.1", - "brotli==1.1.0", - "bump-my-version==1.2.4", - "connect-python-example", - "daphne==4.2.1", - "httpx[http2]==0.28.1", - "hypercorn==0.17.3", - "granian==2.5.7", - "grpcio-tools==1.76.0", - "gunicorn[gevent]==23.0.0", - "just-bin==1.42.4; sys_platform != 'win32'", - "mkdocs==1.6.1", - "mkdocs-material==9.6.20", - "mkdocstrings[python]==0.30.1", - "pyqwest==0.1.0", - "pyright[nodejs]==1.1.405", - "pytest-timeout==2.4.0", - "pyvoy==0.2.0", - "ruff~=0.13.2", - "uvicorn==0.37.0", - # Needed to enable HTTP/2 in daphne - "Twisted[tls,http2]==25.5.0", - "typing_extensions==4.15.0", - "zstandard==0.25.0", - - # Versions locked in constraint-dependencies - "pytest", - "pytest-asyncio", - "pytest-cov", + # Needed to enable HTTP/2 in daphne + "Twisted[tls,http2]==25.5.0", + "asgiref==3.9.1", + "brotli==1.1.0", + "bump-my-version==1.2.4", + "connect-python-example", + "daphne==4.2.1", + "granian==2.5.7", + "grpcio-tools==1.76.0", + "gunicorn[gevent]==23.0.0", + "httpx[http2]==0.28.1", + "hypercorn==0.17.3", + "just-bin==1.42.4; sys_platform != 'win32'", + "pyqwest==0.1.0", + "pyright[nodejs]==1.1.405", + # Versions locked in constraint-dependencies + "pytest", + "pytest-asyncio", + "pytest-cov", + "pytest-timeout==2.4.0", + "pyvoy==0.2.0", + "ruff~=0.13.2", + "typing_extensions==4.15.0", + "uvicorn==0.37.0", + "zstandard==0.25.0", ] [build-system] @@ -74,9 +70,9 @@ build-backend = "uv_build" [tool.uv] resolution = "lowest-direct" constraint-dependencies = [ - "pytest==8.4.2", - "pytest-asyncio==1.2.0", - "pytest-cov==7.0.0", + "pytest==8.4.2", + "pytest-asyncio==1.2.0", + "pytest-cov==7.0.0", ] [tool.uv.build-backend] @@ -84,7 +80,7 @@ module-name = "connectrpc" [tool.pytest.ini_options] testpaths = ["test"] -timeout = 1800 # 30 min +timeout = 1800 # 30 min [tool.ruff.format] skip-magic-trailing-comma = true @@ -92,129 +88,127 @@ docstring-code-format = true [tool.ruff.lint] extend-select = [ - # Same order as listed on https://docs.astral.sh/ruff/rules/ - "YTT", - "ANN", - "ASYNC", - "S", - "FBT", - "B", - "A", - "COM818", # Other comma rules are handled by formatting - "C4", - "DTZ", - "T10", - "EM", - "EXE", - "FA", - "ISC", - "ICN", - "LOG", - "G", - "INP", - "PIE", - "T20", - "PYI", - "PT", - "Q004", # Other quote rules are handled by formatting - "RSE", - "RET", - # This rule is a bit strict since accessing private members within this package can be - # useful to reduce public API exposure. But it is important to not access private members - # of dependencies like httpx so we enable it and ignore the locations where we need to. - "SLF", - "SIM", - "SLOT", - "TID", - # TODO: Update TODOs then enable - # "TD", - "TC", - "ARG", - "PTH", - "FLY", - "I", - "N", - "PERF", - "E", - "W", - # TODO: Flesh out docs and enable - # "DOC" - # "D", - "F", - "PGH", - "PLC", - "PLE", - "PLW", - "UP", - "FURB", - "RUF", - # TODO: See if it makes sense to enable after a pass at reducing code duplication - # "TRY", + # Same order as listed on https://docs.astral.sh/ruff/rules/ + "YTT", + "ANN", + "ASYNC", + "S", + "FBT", + "B", + "A", + "COM818", # Other comma rules are handled by formatting + "C4", + "DTZ", + "T10", + "EM", + "EXE", + "FA", + "ISC", + "ICN", + "LOG", + "G", + "INP", + "PIE", + "T20", + "PYI", + "PT", + "Q004", # Other quote rules are handled by formatting + "RSE", + "RET", + # This rule is a bit strict since accessing private members within this package can be + # useful to reduce public API exposure. But it is important to not access private members + # of dependencies like httpx so we enable it and ignore the locations where we need to. + "SLF", + "SIM", + "SLOT", + "TID", + # TODO: Update TODOs then enable + # "TD", + "TC", + "ARG", + "PTH", + "FLY", + "I", + "N", + "PERF", + "E", + "W", + # TODO: Flesh out docs and enable + # "DOC" + # "D", + "F", + "PGH", + "PLC", + "PLE", + "PLW", + "UP", + "FURB", + "RUF", + # TODO: See if it makes sense to enable after a pass at reducing code duplication + # "TRY", ] - # Document reasons for ignoring specific linting errors extend-ignore = [ - # Not applicable - "AIR", - # Dangerous false positives https://github.com/astral-sh/ruff/issues/4845 - "ERA", - # Not Applicable - "FAST", - # Important to call user callbacks safely - "BLE", - # stdlib includes a module named code. This is less of an issue since users need to import a module so allow it - "A005", - # TODO: Consider using copyright headers - "CPY", - # Not Applicable - "DJ", - # It's fine to have TODOs - "FIX", - # Not Applicable - "INT", - # Even prevents pytest.raises around an iteration which is too strict - "PT012", - # Triggers for protocol implementations that don't need the arg too - "ARG002", - # Complexity checks usually reduce readability - "C90", - # Not Applicable - "NPY", - # Not Applicable - "PD", - # We use the Exception suffix instead - "N818", - # Handled by formatting - "E111", - "E114", - "E117", - "E501", - "W191", - # Low signal-to-noise ratio - "PLR", + # Not applicable + "AIR", + # Dangerous false positives https://github.com/astral-sh/ruff/issues/4845 + "ERA", + # Not Applicable + "FAST", + # Important to call user callbacks safely + "BLE", + # stdlib includes a module named code. This is less of an issue since users need to import a module so allow it + "A005", + # TODO: Consider using copyright headers + "CPY", + # Not Applicable + "DJ", + # It's fine to have TODOs + "FIX", + # Not Applicable + "INT", + # Even prevents pytest.raises around an iteration which is too strict + "PT012", + # Triggers for protocol implementations that don't need the arg too + "ARG002", + # Complexity checks usually reduce readability + "C90", + # Not Applicable + "NPY", + # Not Applicable + "PD", + # We use the Exception suffix instead + "N818", + # Handled by formatting + "E111", + "E114", + "E117", + "E501", + "W191", + # Low signal-to-noise ratio + "PLR", ] - typing-extensions = false [tool.ruff.lint.per-file-ignores] "conformance/test/**" = ["ANN", "INP", "SLF", "SIM115", "S101", "D"] "example/**" = [ - "ANN", - "S", # Keep examples simpler, e.g. allow normal random - "T20", - "D", + "ANN", + "S", # Keep examples simpler, e.g. allow normal random + "T20", + "D", ] "**/test_*.py" = [ - "ANN", - "S101", - "S603", - "S607", - "FBT", - "EM", - "INP", - "SLF", - "PERF", - "D", + "ANN", + "S101", + "S603", + "S607", + "FBT", + "EM", + "INP", + "SLF", + "PERF", + "D", ] "**/*_grpc.py" = ["N", "FBT"] @@ -228,17 +222,15 @@ extend-exclude = ["*_pb2.py", "*_pb2.pyi"] [tool.pyright] exclude = [ - # Defaults. - "**/node_modules", - "**/__pycache__", - "**/.*", - - # GRPC python files don't typecheck on their own. - # See https://github.com/grpc/grpc/issues/39555 - "**/*_pb2_grpc.py", - - # TODO: Work out the import issues to allow it to work. - "conformance/**", + # Defaults. + "**/node_modules", + "**/__pycache__", + "**/.*", + # GRPC python files don't typecheck on their own. + # See https://github.com/grpc/grpc/issues/39555 + "**/*_pb2_grpc.py", + # TODO: Work out the import issues to allow it to work. + "conformance/**", ] [tool.uv.workspace] @@ -251,8 +243,8 @@ connect-python-example = { workspace = true } [tool.bumpversion] current_version = "0.7.1" files = [ - { filename = "pyproject.toml" }, - { filename = "protoc-gen-connect-python/pyproject.toml" }, + { filename = "pyproject.toml" }, + { filename = "protoc-gen-connect-python/pyproject.toml" }, ] parse = "(?P\\d+)\\.(?P\\d+)\\.(?P\\d+)" serialize = ["{major}.{minor}.{patch}"] diff --git a/uv.lock b/uv.lock index 147ea34..9184e9d 100644 --- a/uv.lock +++ b/uv.lock @@ -125,15 +125,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/02/ff/1175b0b7371e46244032d43a56862d0af455823b5280a50c63d99cc50f18/automat-25.4.16-py3-none-any.whl", hash = "sha256:04e9bce696a8d5671ee698005af6e5a9fa15354140a87f4870744604dcdd3ba1", size = 42842, upload-time = "2025-04-16T20:12:14.447Z" }, ] -[[package]] -name = "babel" -version = "2.17.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7d/6b/d52e42361e1aa00709585ecc30b3f9684b3ab62530771402248b1b1d6240/babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d", size = 9951852, upload-time = "2025-02-01T15:17:41.026Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537, upload-time = "2025-02-01T15:17:37.39Z" }, -] - [[package]] name = "backports-asyncio-runner" version = "1.2.0" @@ -143,20 +134,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a0/59/76ab57e3fe74484f48a53f8e337171b4a2349e506eabe136d7e01d059086/backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5", size = 12313, upload-time = "2025-07-02T02:27:14.263Z" }, ] -[[package]] -name = "backrefs" -version = "5.9" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/eb/a7/312f673df6a79003279e1f55619abbe7daebbb87c17c976ddc0345c04c7b/backrefs-5.9.tar.gz", hash = "sha256:808548cb708d66b82ee231f962cb36faaf4f2baab032f2fbb783e9c2fdddaa59", size = 5765857, upload-time = "2025-06-22T19:34:13.97Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/19/4d/798dc1f30468134906575156c089c492cf79b5a5fd373f07fe26c4d046bf/backrefs-5.9-py310-none-any.whl", hash = "sha256:db8e8ba0e9de81fcd635f440deab5ae5f2591b54ac1ebe0550a2ca063488cd9f", size = 380267, upload-time = "2025-06-22T19:34:05.252Z" }, - { url = "https://files.pythonhosted.org/packages/55/07/f0b3375bf0d06014e9787797e6b7cc02b38ac9ff9726ccfe834d94e9991e/backrefs-5.9-py311-none-any.whl", hash = "sha256:6907635edebbe9b2dc3de3a2befff44d74f30a4562adbb8b36f21252ea19c5cf", size = 392072, upload-time = "2025-06-22T19:34:06.743Z" }, - { url = "https://files.pythonhosted.org/packages/9d/12/4f345407259dd60a0997107758ba3f221cf89a9b5a0f8ed5b961aef97253/backrefs-5.9-py312-none-any.whl", hash = "sha256:7fdf9771f63e6028d7fee7e0c497c81abda597ea45d6b8f89e8ad76994f5befa", size = 397947, upload-time = "2025-06-22T19:34:08.172Z" }, - { url = "https://files.pythonhosted.org/packages/10/bf/fa31834dc27a7f05e5290eae47c82690edc3a7b37d58f7fb35a1bdbf355b/backrefs-5.9-py313-none-any.whl", hash = "sha256:cc37b19fa219e93ff825ed1fed8879e47b4d89aa7a1884860e2db64ccd7c676b", size = 399843, upload-time = "2025-06-22T19:34:09.68Z" }, - { url = "https://files.pythonhosted.org/packages/fc/24/b29af34b2c9c41645a9f4ff117bae860291780d73880f449e0b5d948c070/backrefs-5.9-py314-none-any.whl", hash = "sha256:df5e169836cc8acb5e440ebae9aad4bf9d15e226d3bad049cf3f6a5c20cc8dc9", size = 411762, upload-time = "2025-06-22T19:34:11.037Z" }, - { url = "https://files.pythonhosted.org/packages/41/ff/392bff89415399a979be4a65357a41d92729ae8580a66073d8ec8d810f98/backrefs-5.9-py39-none-any.whl", hash = "sha256:f48ee18f6252b8f5777a22a00a09a85de0ca931658f1dd96d4406a34f3748c60", size = 380265, upload-time = "2025-06-22T19:34:12.405Z" }, -] - [[package]] name = "blinker" version = "1.9.0" @@ -356,70 +333,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, ] -[[package]] -name = "charset-normalizer" -version = "3.4.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/83/2d/5fd176ceb9b2fc619e63405525573493ca23441330fcdaee6bef9460e924/charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14", size = 122371, upload-time = "2025-08-09T07:57:28.46Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d6/98/f3b8013223728a99b908c9344da3aa04ee6e3fa235f19409033eda92fb78/charset_normalizer-3.4.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fb7f67a1bfa6e40b438170ebdc8158b78dc465a5a67b6dde178a46987b244a72", size = 207695, upload-time = "2025-08-09T07:55:36.452Z" }, - { url = "https://files.pythonhosted.org/packages/21/40/5188be1e3118c82dcb7c2a5ba101b783822cfb413a0268ed3be0468532de/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc9370a2da1ac13f0153780040f465839e6cccb4a1e44810124b4e22483c93fe", size = 147153, upload-time = "2025-08-09T07:55:38.467Z" }, - { url = "https://files.pythonhosted.org/packages/37/60/5d0d74bc1e1380f0b72c327948d9c2aca14b46a9efd87604e724260f384c/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:07a0eae9e2787b586e129fdcbe1af6997f8d0e5abaa0bc98c0e20e124d67e601", size = 160428, upload-time = "2025-08-09T07:55:40.072Z" }, - { url = "https://files.pythonhosted.org/packages/85/9a/d891f63722d9158688de58d050c59dc3da560ea7f04f4c53e769de5140f5/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:74d77e25adda8581ffc1c720f1c81ca082921329452eba58b16233ab1842141c", size = 157627, upload-time = "2025-08-09T07:55:41.706Z" }, - { url = "https://files.pythonhosted.org/packages/65/1a/7425c952944a6521a9cfa7e675343f83fd82085b8af2b1373a2409c683dc/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0e909868420b7049dafd3a31d45125b31143eec59235311fc4c57ea26a4acd2", size = 152388, upload-time = "2025-08-09T07:55:43.262Z" }, - { url = "https://files.pythonhosted.org/packages/f0/c9/a2c9c2a355a8594ce2446085e2ec97fd44d323c684ff32042e2a6b718e1d/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c6f162aabe9a91a309510d74eeb6507fab5fff92337a15acbe77753d88d9dcf0", size = 150077, upload-time = "2025-08-09T07:55:44.903Z" }, - { url = "https://files.pythonhosted.org/packages/3b/38/20a1f44e4851aa1c9105d6e7110c9d020e093dfa5836d712a5f074a12bf7/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4ca4c094de7771a98d7fbd67d9e5dbf1eb73efa4f744a730437d8a3a5cf994f0", size = 161631, upload-time = "2025-08-09T07:55:46.346Z" }, - { url = "https://files.pythonhosted.org/packages/a4/fa/384d2c0f57edad03d7bec3ebefb462090d8905b4ff5a2d2525f3bb711fac/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:02425242e96bcf29a49711b0ca9f37e451da7c70562bc10e8ed992a5a7a25cc0", size = 159210, upload-time = "2025-08-09T07:55:47.539Z" }, - { url = "https://files.pythonhosted.org/packages/33/9e/eca49d35867ca2db336b6ca27617deed4653b97ebf45dfc21311ce473c37/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:78deba4d8f9590fe4dae384aeff04082510a709957e968753ff3c48399f6f92a", size = 153739, upload-time = "2025-08-09T07:55:48.744Z" }, - { url = "https://files.pythonhosted.org/packages/2a/91/26c3036e62dfe8de8061182d33be5025e2424002125c9500faff74a6735e/charset_normalizer-3.4.3-cp310-cp310-win32.whl", hash = "sha256:d79c198e27580c8e958906f803e63cddb77653731be08851c7df0b1a14a8fc0f", size = 99825, upload-time = "2025-08-09T07:55:50.305Z" }, - { url = "https://files.pythonhosted.org/packages/e2/c6/f05db471f81af1fa01839d44ae2a8bfeec8d2a8b4590f16c4e7393afd323/charset_normalizer-3.4.3-cp310-cp310-win_amd64.whl", hash = "sha256:c6e490913a46fa054e03699c70019ab869e990270597018cef1d8562132c2669", size = 107452, upload-time = "2025-08-09T07:55:51.461Z" }, - { url = "https://files.pythonhosted.org/packages/7f/b5/991245018615474a60965a7c9cd2b4efbaabd16d582a5547c47ee1c7730b/charset_normalizer-3.4.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b256ee2e749283ef3ddcff51a675ff43798d92d746d1a6e4631bf8c707d22d0b", size = 204483, upload-time = "2025-08-09T07:55:53.12Z" }, - { url = "https://files.pythonhosted.org/packages/c7/2a/ae245c41c06299ec18262825c1569c5d3298fc920e4ddf56ab011b417efd/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13faeacfe61784e2559e690fc53fa4c5ae97c6fcedb8eb6fb8d0a15b475d2c64", size = 145520, upload-time = "2025-08-09T07:55:54.712Z" }, - { url = "https://files.pythonhosted.org/packages/3a/a4/b3b6c76e7a635748c4421d2b92c7b8f90a432f98bda5082049af37ffc8e3/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:00237675befef519d9af72169d8604a067d92755e84fe76492fef5441db05b91", size = 158876, upload-time = "2025-08-09T07:55:56.024Z" }, - { url = "https://files.pythonhosted.org/packages/e2/e6/63bb0e10f90a8243c5def74b5b105b3bbbfb3e7bb753915fe333fb0c11ea/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:585f3b2a80fbd26b048a0be90c5aae8f06605d3c92615911c3a2b03a8a3b796f", size = 156083, upload-time = "2025-08-09T07:55:57.582Z" }, - { url = "https://files.pythonhosted.org/packages/87/df/b7737ff046c974b183ea9aa111b74185ac8c3a326c6262d413bd5a1b8c69/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e78314bdc32fa80696f72fa16dc61168fda4d6a0c014e0380f9d02f0e5d8a07", size = 150295, upload-time = "2025-08-09T07:55:59.147Z" }, - { url = "https://files.pythonhosted.org/packages/61/f1/190d9977e0084d3f1dc169acd060d479bbbc71b90bf3e7bf7b9927dec3eb/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:96b2b3d1a83ad55310de8c7b4a2d04d9277d5591f40761274856635acc5fcb30", size = 148379, upload-time = "2025-08-09T07:56:00.364Z" }, - { url = "https://files.pythonhosted.org/packages/4c/92/27dbe365d34c68cfe0ca76f1edd70e8705d82b378cb54ebbaeabc2e3029d/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:939578d9d8fd4299220161fdd76e86c6a251987476f5243e8864a7844476ba14", size = 160018, upload-time = "2025-08-09T07:56:01.678Z" }, - { url = "https://files.pythonhosted.org/packages/99/04/baae2a1ea1893a01635d475b9261c889a18fd48393634b6270827869fa34/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:fd10de089bcdcd1be95a2f73dbe6254798ec1bda9f450d5828c96f93e2536b9c", size = 157430, upload-time = "2025-08-09T07:56:02.87Z" }, - { url = "https://files.pythonhosted.org/packages/2f/36/77da9c6a328c54d17b960c89eccacfab8271fdaaa228305330915b88afa9/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1e8ac75d72fa3775e0b7cb7e4629cec13b7514d928d15ef8ea06bca03ef01cae", size = 151600, upload-time = "2025-08-09T07:56:04.089Z" }, - { url = "https://files.pythonhosted.org/packages/64/d4/9eb4ff2c167edbbf08cdd28e19078bf195762e9bd63371689cab5ecd3d0d/charset_normalizer-3.4.3-cp311-cp311-win32.whl", hash = "sha256:6cf8fd4c04756b6b60146d98cd8a77d0cdae0e1ca20329da2ac85eed779b6849", size = 99616, upload-time = "2025-08-09T07:56:05.658Z" }, - { url = "https://files.pythonhosted.org/packages/f4/9c/996a4a028222e7761a96634d1820de8a744ff4327a00ada9c8942033089b/charset_normalizer-3.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:31a9a6f775f9bcd865d88ee350f0ffb0e25936a7f930ca98995c05abf1faf21c", size = 107108, upload-time = "2025-08-09T07:56:07.176Z" }, - { url = "https://files.pythonhosted.org/packages/e9/5e/14c94999e418d9b87682734589404a25854d5f5d0408df68bc15b6ff54bb/charset_normalizer-3.4.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e28e334d3ff134e88989d90ba04b47d84382a828c061d0d1027b1b12a62b39b1", size = 205655, upload-time = "2025-08-09T07:56:08.475Z" }, - { url = "https://files.pythonhosted.org/packages/7d/a8/c6ec5d389672521f644505a257f50544c074cf5fc292d5390331cd6fc9c3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884", size = 146223, upload-time = "2025-08-09T07:56:09.708Z" }, - { url = "https://files.pythonhosted.org/packages/fc/eb/a2ffb08547f4e1e5415fb69eb7db25932c52a52bed371429648db4d84fb1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018", size = 159366, upload-time = "2025-08-09T07:56:11.326Z" }, - { url = "https://files.pythonhosted.org/packages/82/10/0fd19f20c624b278dddaf83b8464dcddc2456cb4b02bb902a6da126b87a1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cfb2aad70f2c6debfbcb717f23b7eb55febc0bb23dcffc0f076009da10c6392", size = 157104, upload-time = "2025-08-09T07:56:13.014Z" }, - { url = "https://files.pythonhosted.org/packages/16/ab/0233c3231af734f5dfcf0844aa9582d5a1466c985bbed6cedab85af9bfe3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1606f4a55c0fd363d754049cdf400175ee96c992b1f8018b993941f221221c5f", size = 151830, upload-time = "2025-08-09T07:56:14.428Z" }, - { url = "https://files.pythonhosted.org/packages/ae/02/e29e22b4e02839a0e4a06557b1999d0a47db3567e82989b5bb21f3fbbd9f/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:027b776c26d38b7f15b26a5da1044f376455fb3766df8fc38563b4efbc515154", size = 148854, upload-time = "2025-08-09T07:56:16.051Z" }, - { url = "https://files.pythonhosted.org/packages/05/6b/e2539a0a4be302b481e8cafb5af8792da8093b486885a1ae4d15d452bcec/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:42e5088973e56e31e4fa58eb6bd709e42fc03799c11c42929592889a2e54c491", size = 160670, upload-time = "2025-08-09T07:56:17.314Z" }, - { url = "https://files.pythonhosted.org/packages/31/e7/883ee5676a2ef217a40ce0bffcc3d0dfbf9e64cbcfbdf822c52981c3304b/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cc34f233c9e71701040d772aa7490318673aa7164a0efe3172b2981218c26d93", size = 158501, upload-time = "2025-08-09T07:56:18.641Z" }, - { url = "https://files.pythonhosted.org/packages/c1/35/6525b21aa0db614cf8b5792d232021dca3df7f90a1944db934efa5d20bb1/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:320e8e66157cc4e247d9ddca8e21f427efc7a04bbd0ac8a9faf56583fa543f9f", size = 153173, upload-time = "2025-08-09T07:56:20.289Z" }, - { url = "https://files.pythonhosted.org/packages/50/ee/f4704bad8201de513fdc8aac1cabc87e38c5818c93857140e06e772b5892/charset_normalizer-3.4.3-cp312-cp312-win32.whl", hash = "sha256:fb6fecfd65564f208cbf0fba07f107fb661bcd1a7c389edbced3f7a493f70e37", size = 99822, upload-time = "2025-08-09T07:56:21.551Z" }, - { url = "https://files.pythonhosted.org/packages/39/f5/3b3836ca6064d0992c58c7561c6b6eee1b3892e9665d650c803bd5614522/charset_normalizer-3.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:86df271bf921c2ee3818f0522e9a5b8092ca2ad8b065ece5d7d9d0e9f4849bcc", size = 107543, upload-time = "2025-08-09T07:56:23.115Z" }, - { url = "https://files.pythonhosted.org/packages/65/ca/2135ac97709b400c7654b4b764daf5c5567c2da45a30cdd20f9eefe2d658/charset_normalizer-3.4.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe", size = 205326, upload-time = "2025-08-09T07:56:24.721Z" }, - { url = "https://files.pythonhosted.org/packages/71/11/98a04c3c97dd34e49c7d247083af03645ca3730809a5509443f3c37f7c99/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8", size = 146008, upload-time = "2025-08-09T07:56:26.004Z" }, - { url = "https://files.pythonhosted.org/packages/60/f5/4659a4cb3c4ec146bec80c32d8bb16033752574c20b1252ee842a95d1a1e/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9", size = 159196, upload-time = "2025-08-09T07:56:27.25Z" }, - { url = "https://files.pythonhosted.org/packages/86/9e/f552f7a00611f168b9a5865a1414179b2c6de8235a4fa40189f6f79a1753/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31", size = 156819, upload-time = "2025-08-09T07:56:28.515Z" }, - { url = "https://files.pythonhosted.org/packages/7e/95/42aa2156235cbc8fa61208aded06ef46111c4d3f0de233107b3f38631803/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f", size = 151350, upload-time = "2025-08-09T07:56:29.716Z" }, - { url = "https://files.pythonhosted.org/packages/c2/a9/3865b02c56f300a6f94fc631ef54f0a8a29da74fb45a773dfd3dcd380af7/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927", size = 148644, upload-time = "2025-08-09T07:56:30.984Z" }, - { url = "https://files.pythonhosted.org/packages/77/d9/cbcf1a2a5c7d7856f11e7ac2d782aec12bdfea60d104e60e0aa1c97849dc/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9", size = 160468, upload-time = "2025-08-09T07:56:32.252Z" }, - { url = "https://files.pythonhosted.org/packages/f6/42/6f45efee8697b89fda4d50580f292b8f7f9306cb2971d4b53f8914e4d890/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5", size = 158187, upload-time = "2025-08-09T07:56:33.481Z" }, - { url = "https://files.pythonhosted.org/packages/70/99/f1c3bdcfaa9c45b3ce96f70b14f070411366fa19549c1d4832c935d8e2c3/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc", size = 152699, upload-time = "2025-08-09T07:56:34.739Z" }, - { url = "https://files.pythonhosted.org/packages/a3/ad/b0081f2f99a4b194bcbb1934ef3b12aa4d9702ced80a37026b7607c72e58/charset_normalizer-3.4.3-cp313-cp313-win32.whl", hash = "sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce", size = 99580, upload-time = "2025-08-09T07:56:35.981Z" }, - { url = "https://files.pythonhosted.org/packages/9a/8f/ae790790c7b64f925e5c953b924aaa42a243fb778fed9e41f147b2a5715a/charset_normalizer-3.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef", size = 107366, upload-time = "2025-08-09T07:56:37.339Z" }, - { url = "https://files.pythonhosted.org/packages/8e/91/b5a06ad970ddc7a0e513112d40113e834638f4ca1120eb727a249fb2715e/charset_normalizer-3.4.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15", size = 204342, upload-time = "2025-08-09T07:56:38.687Z" }, - { url = "https://files.pythonhosted.org/packages/ce/ec/1edc30a377f0a02689342f214455c3f6c2fbedd896a1d2f856c002fc3062/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db", size = 145995, upload-time = "2025-08-09T07:56:40.048Z" }, - { url = "https://files.pythonhosted.org/packages/17/e5/5e67ab85e6d22b04641acb5399c8684f4d37caf7558a53859f0283a650e9/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d", size = 158640, upload-time = "2025-08-09T07:56:41.311Z" }, - { url = "https://files.pythonhosted.org/packages/f1/e5/38421987f6c697ee3722981289d554957c4be652f963d71c5e46a262e135/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096", size = 156636, upload-time = "2025-08-09T07:56:43.195Z" }, - { url = "https://files.pythonhosted.org/packages/a0/e4/5a075de8daa3ec0745a9a3b54467e0c2967daaaf2cec04c845f73493e9a1/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa", size = 150939, upload-time = "2025-08-09T07:56:44.819Z" }, - { url = "https://files.pythonhosted.org/packages/02/f7/3611b32318b30974131db62b4043f335861d4d9b49adc6d57c1149cc49d4/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049", size = 148580, upload-time = "2025-08-09T07:56:46.684Z" }, - { url = "https://files.pythonhosted.org/packages/7e/61/19b36f4bd67f2793ab6a99b979b4e4f3d8fc754cbdffb805335df4337126/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0", size = 159870, upload-time = "2025-08-09T07:56:47.941Z" }, - { url = "https://files.pythonhosted.org/packages/06/57/84722eefdd338c04cf3030ada66889298eaedf3e7a30a624201e0cbe424a/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92", size = 157797, upload-time = "2025-08-09T07:56:49.756Z" }, - { url = "https://files.pythonhosted.org/packages/72/2a/aff5dd112b2f14bcc3462c312dce5445806bfc8ab3a7328555da95330e4b/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16", size = 152224, upload-time = "2025-08-09T07:56:51.369Z" }, - { url = "https://files.pythonhosted.org/packages/b7/8c/9839225320046ed279c6e839d51f028342eb77c91c89b8ef2549f951f3ec/charset_normalizer-3.4.3-cp314-cp314-win32.whl", hash = "sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce", size = 100086, upload-time = "2025-08-09T07:56:52.722Z" }, - { url = "https://files.pythonhosted.org/packages/ee/7a/36fbcf646e41f710ce0a563c1c9a343c6edf9be80786edeb15b6f62e17db/charset_normalizer-3.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c", size = 107400, upload-time = "2025-08-09T07:56:55.172Z" }, - { url = "https://files.pythonhosted.org/packages/8a/1f/f041989e93b001bc4e44bb1669ccdcf54d3f00e628229a85b08d330615c5/charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a", size = 53175, upload-time = "2025-08-09T07:57:26.864Z" }, -] - [[package]] name = "click" version = "8.2.1" @@ -463,9 +376,6 @@ dev = [ { name = "httpx", extra = ["http2"] }, { name = "hypercorn" }, { name = "just-bin", marker = "sys_platform != 'win32'" }, - { name = "mkdocs" }, - { name = "mkdocs-material" }, - { name = "mkdocstrings", extra = ["python"] }, { name = "pyqwest" }, { name = "pyright", extra = ["nodejs"] }, { name = "pytest" }, @@ -499,9 +409,6 @@ dev = [ { name = "httpx", extras = ["http2"], specifier = "==0.28.1" }, { name = "hypercorn", specifier = "==0.17.3" }, { name = "just-bin", marker = "sys_platform != 'win32'", specifier = "==1.42.4" }, - { name = "mkdocs", specifier = "==1.6.1" }, - { name = "mkdocs-material", specifier = "==9.6.20" }, - { name = "mkdocstrings", extras = ["python"], specifier = "==0.30.1" }, { name = "pyqwest", specifier = "==0.1.0" }, { name = "pyright", extras = ["nodejs"], specifier = "==1.1.405" }, { name = "pytest" }, @@ -836,18 +743,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c2/69/a7c4ba2ffbc7c7dbf6d8b4f5d0f0a421f7815d229f4909854266c445a3d4/gevent-25.9.1-cp314-cp314-win_amd64.whl", hash = "sha256:bb63c0d6cb9950cc94036a4995b9cc4667b8915366613449236970f4394f94d7", size = 1703019, upload-time = "2025-09-17T19:30:55.272Z" }, ] -[[package]] -name = "ghp-import" -version = "2.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "python-dateutil" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/d9/29/d40217cbe2f6b1359e00c6c307bb3fc876ba74068cbab3dde77f03ca0dc4/ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343", size = 10943, upload-time = "2022-05-02T15:47:16.11Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034, upload-time = "2022-05-02T15:47:14.552Z" }, -] - [[package]] name = "granian" version = "2.5.7" @@ -1002,18 +897,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e3/a5/6ddab2b4c112be95601c13428db1d8b6608a8b6039816f2ba09c346c08fc/greenlet-3.2.4-cp314-cp314-win_amd64.whl", hash = "sha256:e37ab26028f12dbb0ff65f29a8d3d44a765c61e729647bf2ddfbbed621726f01", size = 303425, upload-time = "2025-08-07T13:32:27.59Z" }, ] -[[package]] -name = "griffe" -version = "1.14.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ec/d7/6c09dd7ce4c7837e4cdb11dce980cb45ae3cd87677298dc3b781b6bce7d3/griffe-1.14.0.tar.gz", hash = "sha256:9d2a15c1eca966d68e00517de5d69dd1bc5c9f2335ef6c1775362ba5b8651a13", size = 424684, upload-time = "2025-09-05T15:02:29.167Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/b1/9ff6578d789a89812ff21e4e0f80ffae20a65d5dd84e7a17873fe3b365be/griffe-1.14.0-py3-none-any.whl", hash = "sha256:0e9d52832cccf0f7188cfe585ba962d2674b241c01916d780925df34873bceb0", size = 144439, upload-time = "2025-09-05T15:02:27.511Z" }, -] - [[package]] name = "grpcio" version = "1.76.0" @@ -1322,15 +1205,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/52/14/d51fdc5a5f005325cac39849ec5fb7ccfebfa2b07ff111eaabe045954233/just_bin-1.42.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:384dd54bf7a4a77949a84f2c357f5506d917240e7b17672959ee42bcb52e11ea", size = 1762718, upload-time = "2025-08-20T02:23:28.848Z" }, ] -[[package]] -name = "markdown" -version = "3.9" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8d/37/02347f6d6d8279247a5837082ebc26fc0d5aaeaf75aa013fcbb433c777ab/markdown-3.9.tar.gz", hash = "sha256:d2900fe1782bd33bdbbd56859defef70c2e78fc46668f8eb9df3128138f2cb6a", size = 364585, upload-time = "2025-09-04T20:25:22.885Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/70/ae/44c4a6a4cbb496d93c6257954260fe3a6e91b7bed2240e5dad2a717f5111/markdown-3.9-py3-none-any.whl", hash = "sha256:9f4d91ed810864ea88a6f32c07ba8bee1346c0cc1f6b1f9f6c822f2a9667d280", size = 107441, upload-time = "2025-09-04T20:25:21.784Z" }, -] - [[package]] name = "markdown-it-py" version = "4.0.0" @@ -1410,136 +1284,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, ] -[[package]] -name = "mergedeep" -version = "1.3.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3a/41/580bb4006e3ed0361b8151a01d324fb03f420815446c7def45d02f74c270/mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", size = 4661, upload-time = "2021-02-05T18:55:30.623Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354, upload-time = "2021-02-05T18:55:29.583Z" }, -] - -[[package]] -name = "mkdocs" -version = "1.6.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "ghp-import" }, - { name = "jinja2" }, - { name = "markdown" }, - { name = "markupsafe" }, - { name = "mergedeep" }, - { name = "mkdocs-get-deps" }, - { name = "packaging" }, - { name = "pathspec" }, - { name = "pyyaml" }, - { name = "pyyaml-env-tag" }, - { name = "watchdog" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/bc/c6/bbd4f061bd16b378247f12953ffcb04786a618ce5e904b8c5a01a0309061/mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2", size = 3889159, upload-time = "2024-08-30T12:24:06.899Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e", size = 3864451, upload-time = "2024-08-30T12:24:05.054Z" }, -] - -[[package]] -name = "mkdocs-autorefs" -version = "1.4.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markdown" }, - { name = "markupsafe" }, - { name = "mkdocs" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/51/fa/9124cd63d822e2bcbea1450ae68cdc3faf3655c69b455f3a7ed36ce6c628/mkdocs_autorefs-1.4.3.tar.gz", hash = "sha256:beee715b254455c4aa93b6ef3c67579c399ca092259cc41b7d9342573ff1fc75", size = 55425, upload-time = "2025-08-26T14:23:17.223Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9f/4d/7123b6fa2278000688ebd338e2a06d16870aaf9eceae6ba047ea05f92df1/mkdocs_autorefs-1.4.3-py3-none-any.whl", hash = "sha256:469d85eb3114801d08e9cc55d102b3ba65917a869b893403b8987b601cf55dc9", size = 25034, upload-time = "2025-08-26T14:23:15.906Z" }, -] - -[[package]] -name = "mkdocs-get-deps" -version = "0.2.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "mergedeep" }, - { name = "platformdirs" }, - { name = "pyyaml" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/98/f5/ed29cd50067784976f25ed0ed6fcd3c2ce9eb90650aa3b2796ddf7b6870b/mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c", size = 10239, upload-time = "2023-11-20T17:51:09.981Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9f/d4/029f984e8d3f3b6b726bd33cafc473b75e9e44c0f7e80a5b29abc466bdea/mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134", size = 9521, upload-time = "2023-11-20T17:51:08.587Z" }, -] - -[[package]] -name = "mkdocs-material" -version = "9.6.20" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "babel" }, - { name = "backrefs" }, - { name = "click" }, - { name = "colorama" }, - { name = "jinja2" }, - { name = "markdown" }, - { name = "mkdocs" }, - { name = "mkdocs-material-extensions" }, - { name = "paginate" }, - { name = "pygments" }, - { name = "pymdown-extensions" }, - { name = "requests" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ba/ee/6ed7fc739bd7591485c8bec67d5984508d3f2733e708f32714c21593341a/mkdocs_material-9.6.20.tar.gz", hash = "sha256:e1f84d21ec5fb730673c4259b2e0d39f8d32a3fef613e3a8e7094b012d43e790", size = 4037822, upload-time = "2025-09-15T08:48:01.816Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/67/d8/a31dd52e657bf12b20574706d07df8d767e1ab4340f9bfb9ce73950e5e59/mkdocs_material-9.6.20-py3-none-any.whl", hash = "sha256:b8d8c8b0444c7c06dd984b55ba456ce731f0035c5a1533cc86793618eb1e6c82", size = 9193367, upload-time = "2025-09-15T08:47:58.722Z" }, -] - -[[package]] -name = "mkdocs-material-extensions" -version = "1.3.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/79/9b/9b4c96d6593b2a541e1cb8b34899a6d021d208bb357042823d4d2cabdbe7/mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443", size = 11847, upload-time = "2023-11-22T19:09:45.208Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31", size = 8728, upload-time = "2023-11-22T19:09:43.465Z" }, -] - -[[package]] -name = "mkdocstrings" -version = "0.30.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "jinja2" }, - { name = "markdown" }, - { name = "markupsafe" }, - { name = "mkdocs" }, - { name = "mkdocs-autorefs" }, - { name = "pymdown-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c5/33/2fa3243439f794e685d3e694590d28469a9b8ea733af4b48c250a3ffc9a0/mkdocstrings-0.30.1.tar.gz", hash = "sha256:84a007aae9b707fb0aebfc9da23db4b26fc9ab562eb56e335e9ec480cb19744f", size = 106350, upload-time = "2025-09-19T10:49:26.446Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7b/2c/f0dc4e1ee7f618f5bff7e05898d20bf8b6e7fa612038f768bfa295f136a4/mkdocstrings-0.30.1-py3-none-any.whl", hash = "sha256:41bd71f284ca4d44a668816193e4025c950b002252081e387433656ae9a70a82", size = 36704, upload-time = "2025-09-19T10:49:24.805Z" }, -] - -[package.optional-dependencies] -python = [ - { name = "mkdocstrings-python" }, -] - -[[package]] -name = "mkdocstrings-python" -version = "1.18.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "griffe" }, - { name = "mkdocs-autorefs" }, - { name = "mkdocstrings" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/95/ae/58ab2bfbee2792e92a98b97e872f7c003deb903071f75d8d83aa55db28fa/mkdocstrings_python-1.18.2.tar.gz", hash = "sha256:4ad536920a07b6336f50d4c6d5603316fafb1172c5c882370cbbc954770ad323", size = 207972, upload-time = "2025-08-28T16:11:19.847Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d5/8f/ce008599d9adebf33ed144e7736914385e8537f5fc686fdb7cceb8c22431/mkdocstrings_python-1.18.2-py3-none-any.whl", hash = "sha256:944fe6deb8f08f33fa936d538233c4036e9f53e840994f6146e8e94eb71b600d", size = 138215, upload-time = "2025-08-28T16:11:18.176Z" }, -] - [[package]] name = "nodeenv" version = "1.9.1" @@ -1574,33 +1318,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 = "paginate" -version = "0.5.7" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ec/46/68dde5b6bc00c1296ec6466ab27dddede6aec9af1b99090e1107091b3b84/paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945", size = 19252, upload-time = "2024-08-25T14:17:24.139Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591", size = 13746, upload-time = "2024-08-25T14:17:22.55Z" }, -] - -[[package]] -name = "pathspec" -version = "0.12.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, -] - -[[package]] -name = "platformdirs" -version = "4.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/23/e8/21db9c9987b0e728855bd57bff6984f67952bea55d6f75e055c46b5383e8/platformdirs-4.4.0.tar.gz", hash = "sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf", size = 21634, upload-time = "2025-08-26T14:32:04.268Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/40/4b/2028861e724d3bd36227adfa20d3fd24c3fc6d52032f4a93c133be5d17ce/platformdirs-4.4.0-py3-none-any.whl", hash = "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85", size = 18654, upload-time = "2025-08-26T14:32:02.735Z" }, -] - [[package]] name = "pluggy" version = "1.6.0" @@ -1831,19 +1548,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, ] -[[package]] -name = "pymdown-extensions" -version = "10.16.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markdown" }, - { name = "pyyaml" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/55/b3/6d2b3f149bc5413b0a29761c2c5832d8ce904a1d7f621e86616d96f505cc/pymdown_extensions-10.16.1.tar.gz", hash = "sha256:aace82bcccba3efc03e25d584e6a22d27a8e17caa3f4dd9f207e49b787aa9a91", size = 853277, upload-time = "2025-07-28T16:19:34.167Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e4/06/43084e6cbd4b3bc0e80f6be743b2e79fbc6eed8de9ad8c629939fa55d972/pymdown_extensions-10.16.1-py3-none-any.whl", hash = "sha256:d6ba157a6c03146a7fb122b2b9a121300056384eafeec9c9f9e584adfdb2a32d", size = 266178, upload-time = "2025-07-28T16:19:31.401Z" }, -] - [[package]] name = "pyopenssl" version = "25.3.0" @@ -1983,18 +1687,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fa/b6/3127540ecdf1464a00e5a01ee60a1b09175f6913f0644ac748494d9c4b21/pytest_timeout-2.4.0-py3-none-any.whl", hash = "sha256:c42667e5cdadb151aeb5b26d114aff6bdf5a907f176a007a30b940d3d865b5c2", size = 14382, upload-time = "2025-05-05T19:44:33.502Z" }, ] -[[package]] -name = "python-dateutil" -version = "2.9.0.post0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "six" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, -] - [[package]] name = "python-dotenv" version = "1.2.1" @@ -2106,18 +1798,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, ] -[[package]] -name = "pyyaml-env-tag" -version = "1.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyyaml" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/eb/2e/79c822141bfd05a853236b504869ebc6b70159afc570e1d5a20641782eaa/pyyaml_env_tag-1.1.tar.gz", hash = "sha256:2eb38b75a2d21ee0475d6d97ec19c63287a7e140231e4214969d0eac923cd7ff", size = 5737, upload-time = "2025-05-13T15:24:01.64Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl", hash = "sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04", size = 4722, upload-time = "2025-05-13T15:23:59.629Z" }, -] - [[package]] name = "questionary" version = "2.1.1" @@ -2130,21 +1810,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3c/26/1062c7ec1b053db9e499b4d2d5bc231743201b74051c973dadeac80a8f43/questionary-2.1.1-py3-none-any.whl", hash = "sha256:a51af13f345f1cdea62347589fbb6df3b290306ab8930713bfae4d475a7d4a59", size = 36753, upload-time = "2025-08-28T19:00:19.56Z" }, ] -[[package]] -name = "requests" -version = "2.32.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "certifi" }, - { name = "charset-normalizer" }, - { name = "idna" }, - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, -] - [[package]] name = "rich" version = "14.2.0" @@ -2223,15 +1888,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" }, ] -[[package]] -name = "six" -version = "1.17.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, -] - [[package]] name = "sniffio" version = "1.3.1" @@ -2374,15 +2030,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, ] -[[package]] -name = "urllib3" -version = "2.5.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, -] - [[package]] name = "uvicorn" version = "0.37.0" @@ -2441,38 +2088,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e4/16/c1fd27e9549f3c4baf1dc9c20c456cd2f822dbf8de9f463824b0c0357e06/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e", size = 4296730, upload-time = "2025-10-16T22:17:00.744Z" }, ] -[[package]] -name = "watchdog" -version = "6.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0c/56/90994d789c61df619bfc5ce2ecdabd5eeff564e1eb47512bd01b5e019569/watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26", size = 96390, upload-time = "2024-11-01T14:06:24.793Z" }, - { url = "https://files.pythonhosted.org/packages/55/46/9a67ee697342ddf3c6daa97e3a587a56d6c4052f881ed926a849fcf7371c/watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112", size = 88389, upload-time = "2024-11-01T14:06:27.112Z" }, - { url = "https://files.pythonhosted.org/packages/44/65/91b0985747c52064d8701e1075eb96f8c40a79df889e59a399453adfb882/watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3", size = 89020, upload-time = "2024-11-01T14:06:29.876Z" }, - { url = "https://files.pythonhosted.org/packages/e0/24/d9be5cd6642a6aa68352ded4b4b10fb0d7889cb7f45814fb92cecd35f101/watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c", size = 96393, upload-time = "2024-11-01T14:06:31.756Z" }, - { url = "https://files.pythonhosted.org/packages/63/7a/6013b0d8dbc56adca7fdd4f0beed381c59f6752341b12fa0886fa7afc78b/watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2", size = 88392, upload-time = "2024-11-01T14:06:32.99Z" }, - { url = "https://files.pythonhosted.org/packages/d1/40/b75381494851556de56281e053700e46bff5b37bf4c7267e858640af5a7f/watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c", size = 89019, upload-time = "2024-11-01T14:06:34.963Z" }, - { url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471, upload-time = "2024-11-01T14:06:37.745Z" }, - { url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449, upload-time = "2024-11-01T14:06:39.748Z" }, - { url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054, upload-time = "2024-11-01T14:06:41.009Z" }, - { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480, upload-time = "2024-11-01T14:06:42.952Z" }, - { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451, upload-time = "2024-11-01T14:06:45.084Z" }, - { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057, upload-time = "2024-11-01T14:06:47.324Z" }, - { url = "https://files.pythonhosted.org/packages/30/ad/d17b5d42e28a8b91f8ed01cb949da092827afb9995d4559fd448d0472763/watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881", size = 87902, upload-time = "2024-11-01T14:06:53.119Z" }, - { url = "https://files.pythonhosted.org/packages/5c/ca/c3649991d140ff6ab67bfc85ab42b165ead119c9e12211e08089d763ece5/watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11", size = 88380, upload-time = "2024-11-01T14:06:55.19Z" }, - { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" }, - { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" }, - { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" }, - { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" }, - { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" }, - { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" }, - { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" }, - { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload-time = "2024-11-01T14:07:09.525Z" }, - { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" }, - { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" }, -] - [[package]] name = "wcmatch" version = "10.1" From caef34c2bc7e21773786a00bf543618cbc82e4fe Mon Sep 17 00:00:00 2001 From: Stefan VanBuren Date: Tue, 13 Jan 2026 10:47:10 -0500 Subject: [PATCH 2/3] Revert pyproject.toml diff `tombi` formatting from my LSP; :facepalm:. Signed-off-by: Stefan VanBuren --- pyproject.toml | 351 +++++++++++++++++++++++++------------------------ 1 file changed, 178 insertions(+), 173 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index fd740b6..20d66e8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,65 +2,66 @@ name = "connect-python" version = "0.7.1" description = "Server and client runtime library for Connect RPC" -readme = "README.md" -requires-python = ">= 3.10" -license-files = ["LICENSE"] maintainers = [ - { name = "Anuraag Agrawal", email = "anuraaga@gmail.com" }, - { name = "Spencer Nelson", email = "spencer@firetiger.com" }, - { name = "Stefan VanBuren", email = "svanburen@buf.build" }, - { name = "Yasushi Itoh", email = "i2y.may.roku@gmail.com" }, + { name = "Anuraag Agrawal", email = "anuraaga@gmail.com" }, + { name = "Spencer Nelson", email = "spencer@firetiger.com" }, + { name = "Stefan VanBuren", email = "svanburen@buf.build" }, + { name = "Yasushi Itoh", email = "i2y.may.roku@gmail.com" }, ] -keywords = ["connect", "grpc", "http", "protobuf", "rpc"] +requires-python = ">= 3.10" +dependencies = ["httpx>=0.28.1", "protobuf>=5.28"] +readme = "README.md" +license-files = ["LICENSE"] +keywords = ["rpc", "grpc", "connect", "protobuf", "http"] classifiers = [ - "Development Status :: 3 - Alpha", - "Intended Audience :: Developers", - "License :: OSI Approved :: Apache Software License", - "Operating System :: OS Independent", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Programming Language :: Python :: 3.13", - "Programming Language :: Python :: 3.14", - "Topic :: Internet :: WWW/HTTP", - "Topic :: Software Development :: Libraries :: Python Modules", - "Typing :: Typed", + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "Topic :: Internet :: WWW/HTTP", + "Topic :: Software Development :: Libraries :: Python Modules", + "Typing :: Typed", ] -dependencies = ["httpx>=0.28.1", "protobuf>=5.28"] [project.urls] Homepage = "https://github.com/connectrpc/connect-python" -Issues = "https://github.com/connectrpc/connect-python/issues" Repository = "https://github.com/connectrpc/connect-python" +Issues = "https://github.com/connectrpc/connect-python/issues" [dependency-groups] dev = [ - # Needed to enable HTTP/2 in daphne - "Twisted[tls,http2]==25.5.0", - "asgiref==3.9.1", - "brotli==1.1.0", - "bump-my-version==1.2.4", - "connect-python-example", - "daphne==4.2.1", - "granian==2.5.7", - "grpcio-tools==1.76.0", - "gunicorn[gevent]==23.0.0", - "httpx[http2]==0.28.1", - "hypercorn==0.17.3", - "just-bin==1.42.4; sys_platform != 'win32'", - "pyqwest==0.1.0", - "pyright[nodejs]==1.1.405", - # Versions locked in constraint-dependencies - "pytest", - "pytest-asyncio", - "pytest-cov", - "pytest-timeout==2.4.0", - "pyvoy==0.2.0", - "ruff~=0.13.2", - "typing_extensions==4.15.0", - "uvicorn==0.37.0", - "zstandard==0.25.0", + "asgiref==3.9.1", + "brotli==1.1.0", + "bump-my-version==1.2.4", + "connect-python-example", + "daphne==4.2.1", + "httpx[http2]==0.28.1", + "hypercorn==0.17.3", + "granian==2.5.7", + "grpcio-tools==1.76.0", + "gunicorn[gevent]==23.0.0", + "just-bin==1.42.4; sys_platform != 'win32'", + "pyqwest==0.1.0", + "pyright[nodejs]==1.1.405", + "pytest-timeout==2.4.0", + "pyvoy==0.2.0", + "ruff~=0.13.2", + "uvicorn==0.37.0", + # Needed to enable HTTP/2 in daphne + "Twisted[tls,http2]==25.5.0", + "typing_extensions==4.15.0", + "zstandard==0.25.0", + + # Versions locked in constraint-dependencies + "pytest", + "pytest-asyncio", + "pytest-cov", ] [build-system] @@ -70,9 +71,9 @@ build-backend = "uv_build" [tool.uv] resolution = "lowest-direct" constraint-dependencies = [ - "pytest==8.4.2", - "pytest-asyncio==1.2.0", - "pytest-cov==7.0.0", + "pytest==8.4.2", + "pytest-asyncio==1.2.0", + "pytest-cov==7.0.0", ] [tool.uv.build-backend] @@ -80,7 +81,7 @@ module-name = "connectrpc" [tool.pytest.ini_options] testpaths = ["test"] -timeout = 1800 # 30 min +timeout = 1800 # 30 min [tool.ruff.format] skip-magic-trailing-comma = true @@ -88,127 +89,129 @@ docstring-code-format = true [tool.ruff.lint] extend-select = [ - # Same order as listed on https://docs.astral.sh/ruff/rules/ - "YTT", - "ANN", - "ASYNC", - "S", - "FBT", - "B", - "A", - "COM818", # Other comma rules are handled by formatting - "C4", - "DTZ", - "T10", - "EM", - "EXE", - "FA", - "ISC", - "ICN", - "LOG", - "G", - "INP", - "PIE", - "T20", - "PYI", - "PT", - "Q004", # Other quote rules are handled by formatting - "RSE", - "RET", - # This rule is a bit strict since accessing private members within this package can be - # useful to reduce public API exposure. But it is important to not access private members - # of dependencies like httpx so we enable it and ignore the locations where we need to. - "SLF", - "SIM", - "SLOT", - "TID", - # TODO: Update TODOs then enable - # "TD", - "TC", - "ARG", - "PTH", - "FLY", - "I", - "N", - "PERF", - "E", - "W", - # TODO: Flesh out docs and enable - # "DOC" - # "D", - "F", - "PGH", - "PLC", - "PLE", - "PLW", - "UP", - "FURB", - "RUF", - # TODO: See if it makes sense to enable after a pass at reducing code duplication - # "TRY", + # Same order as listed on https://docs.astral.sh/ruff/rules/ + "YTT", + "ANN", + "ASYNC", + "S", + "FBT", + "B", + "A", + "COM818", # Other comma rules are handled by formatting + "C4", + "DTZ", + "T10", + "EM", + "EXE", + "FA", + "ISC", + "ICN", + "LOG", + "G", + "INP", + "PIE", + "T20", + "PYI", + "PT", + "Q004", # Other quote rules are handled by formatting + "RSE", + "RET", + # This rule is a bit strict since accessing private members within this package can be + # useful to reduce public API exposure. But it is important to not access private members + # of dependencies like httpx so we enable it and ignore the locations where we need to. + "SLF", + "SIM", + "SLOT", + "TID", + # TODO: Update TODOs then enable + # "TD", + "TC", + "ARG", + "PTH", + "FLY", + "I", + "N", + "PERF", + "E", + "W", + # TODO: Flesh out docs and enable + # "DOC" + # "D", + "F", + "PGH", + "PLC", + "PLE", + "PLW", + "UP", + "FURB", + "RUF", + # TODO: See if it makes sense to enable after a pass at reducing code duplication + # "TRY", ] + # Document reasons for ignoring specific linting errors extend-ignore = [ - # Not applicable - "AIR", - # Dangerous false positives https://github.com/astral-sh/ruff/issues/4845 - "ERA", - # Not Applicable - "FAST", - # Important to call user callbacks safely - "BLE", - # stdlib includes a module named code. This is less of an issue since users need to import a module so allow it - "A005", - # TODO: Consider using copyright headers - "CPY", - # Not Applicable - "DJ", - # It's fine to have TODOs - "FIX", - # Not Applicable - "INT", - # Even prevents pytest.raises around an iteration which is too strict - "PT012", - # Triggers for protocol implementations that don't need the arg too - "ARG002", - # Complexity checks usually reduce readability - "C90", - # Not Applicable - "NPY", - # Not Applicable - "PD", - # We use the Exception suffix instead - "N818", - # Handled by formatting - "E111", - "E114", - "E117", - "E501", - "W191", - # Low signal-to-noise ratio - "PLR", + # Not applicable + "AIR", + # Dangerous false positives https://github.com/astral-sh/ruff/issues/4845 + "ERA", + # Not Applicable + "FAST", + # Important to call user callbacks safely + "BLE", + # stdlib includes a module named code. This is less of an issue since users need to import a module so allow it + "A005", + # TODO: Consider using copyright headers + "CPY", + # Not Applicable + "DJ", + # It's fine to have TODOs + "FIX", + # Not Applicable + "INT", + # Even prevents pytest.raises around an iteration which is too strict + "PT012", + # Triggers for protocol implementations that don't need the arg too + "ARG002", + # Complexity checks usually reduce readability + "C90", + # Not Applicable + "NPY", + # Not Applicable + "PD", + # We use the Exception suffix instead + "N818", + # Handled by formatting + "E111", + "E114", + "E117", + "E501", + "W191", + # Low signal-to-noise ratio + "PLR", ] + typing-extensions = false [tool.ruff.lint.per-file-ignores] "conformance/test/**" = ["ANN", "INP", "SLF", "SIM115", "S101", "D"] "example/**" = [ - "ANN", - "S", # Keep examples simpler, e.g. allow normal random - "T20", - "D", + "ANN", + "S", # Keep examples simpler, e.g. allow normal random + "T20", + "D", ] "**/test_*.py" = [ - "ANN", - "S101", - "S603", - "S607", - "FBT", - "EM", - "INP", - "SLF", - "PERF", - "D", + "ANN", + "S101", + "S603", + "S607", + "FBT", + "EM", + "INP", + "SLF", + "PERF", + "D", ] "**/*_grpc.py" = ["N", "FBT"] @@ -222,15 +225,17 @@ extend-exclude = ["*_pb2.py", "*_pb2.pyi"] [tool.pyright] exclude = [ - # Defaults. - "**/node_modules", - "**/__pycache__", - "**/.*", - # GRPC python files don't typecheck on their own. - # See https://github.com/grpc/grpc/issues/39555 - "**/*_pb2_grpc.py", - # TODO: Work out the import issues to allow it to work. - "conformance/**", + # Defaults. + "**/node_modules", + "**/__pycache__", + "**/.*", + + # GRPC python files don't typecheck on their own. + # See https://github.com/grpc/grpc/issues/39555 + "**/*_pb2_grpc.py", + + # TODO: Work out the import issues to allow it to work. + "conformance/**", ] [tool.uv.workspace] @@ -243,8 +248,8 @@ connect-python-example = { workspace = true } [tool.bumpversion] current_version = "0.7.1" files = [ - { filename = "pyproject.toml" }, - { filename = "protoc-gen-connect-python/pyproject.toml" }, + { filename = "pyproject.toml" }, + { filename = "protoc-gen-connect-python/pyproject.toml" }, ] parse = "(?P\\d+)\\.(?P\\d+)\\.(?P\\d+)" serialize = ["{major}.{minor}.{patch}"] From 5317157936c893fe5c935506107f3d8b607eebd2 Mon Sep 17 00:00:00 2001 From: Stefan VanBuren Date: Tue, 13 Jan 2026 20:12:31 -0500 Subject: [PATCH 3/3] Remove mkdocs.yml Also, update the DEVELOPMENT.md to remove the old `justfile` targets and mention that documentation is now in the upstream connectrpc.com repo. Signed-off-by: Stefan VanBuren --- DEVELOPMENT.md | 17 +---------------- mkdocs.yml | 33 --------------------------------- 2 files changed, 1 insertion(+), 49 deletions(-) delete mode 100644 mkdocs.yml diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 15e78aa..9864b26 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -83,22 +83,7 @@ To release a new version, follow the guide in [RELEASE.md](./RELEASE.md). ## Documentation -### Building Documentation - -```bash -# Build documentation -uv run just docs - -# Serve documentation locally -uv run just docs-serve -``` - -### Writing Documentation - -- Use MyST markdown for documentation files -- Place API documentation in `docs/api.md` -- Place examples in `docs/examples.md` -- Update the main `docs/index.md` for structural changes +Documentation is contained in the [connectrpc/connectrpc.com](https://github.com/connectrpc/connectrpc.com) repository. ## Contributing diff --git a/mkdocs.yml b/mkdocs.yml deleted file mode 100644 index a050096..0000000 --- a/mkdocs.yml +++ /dev/null @@ -1,33 +0,0 @@ -site_name: Connect Documentation - -theme: - name: material - features: - - content.code.copy - - content.tabs.link - -plugins: - - mkdocstrings: - handlers: - python: - options: - docstring_options: - ignore_init_summary: true - members_order: source - merge_init_into_class: true - show_signature: false - inventories: - - https://docs.python.org/3/objects.inv - - https://googleapis.dev/python/protobuf/latest/objects.inv - -markdown_extensions: - - pymdownx.highlight: - default_lang: python - anchor_linenums: true - line_spans: __span - pygments_lang_class: true - - pymdownx.inlinehilite - - pymdownx.snippets - - pymdownx.superfences: - - pymdownx.tabbed: - alternate_style: true