{"id":19787973,"url":"https://github.com/i2y/connecpy","last_synced_at":"2025-05-01T00:30:36.572Z","repository":{"id":216518188,"uuid":"741036248","full_name":"i2y/connecpy","owner":"i2y","description":"Python implementation of Connect Protocol","archived":false,"fork":false,"pushed_at":"2025-03-01T06:33:40.000Z","size":244,"stargazers_count":26,"open_issues_count":2,"forks_count":3,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-04-06T06:25:09.546Z","etag":null,"topics":["connectrpc","protobuf","protocol-buffers","python","rpc"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"unlicense","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/i2y.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-01-09T15:12:00.000Z","updated_at":"2025-03-22T15:27:38.000Z","dependencies_parsed_at":"2024-02-23T16:30:06.794Z","dependency_job_id":"10776523-1568-445b-b7b6-f96bf465b5b5","html_url":"https://github.com/i2y/connecpy","commit_stats":null,"previous_names":["i2y/connecpy"],"tags_count":11,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/i2y%2Fconnecpy","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/i2y%2Fconnecpy/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/i2y%2Fconnecpy/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/i2y%2Fconnecpy/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/i2y","download_url":"https://codeload.github.com/i2y/connecpy/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":251802693,"owners_count":21646260,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["connectrpc","protobuf","protocol-buffers","python","rpc"],"created_at":"2024-11-12T06:25:33.156Z","updated_at":"2025-05-01T00:30:36.564Z","avatar_url":"https://github.com/i2y.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Connecpy\n\nPython implementation of [Connect Protocol](https://connectrpc.com/docs/protocol).\n\nThis repo contains a protoc plugin that generates sever and client code and a pypi package with common implementation details.\n\n## Installation\n\nYou can install the protoc plugin to generate files by running the command:\n\n```sh\ngo install github.com/i2y/connecpy/protoc-gen-connecpy@latest\n```\n\nAdditionally, please add the connecpy package to your project using your preferred package manager. For instance, with [uv](https://docs.astral.sh/uv/), use the command:\n\n\n```sh\nuv add connecpy\n```\n\nor\n\n```sh\npip install connecpy\n```\n\n\nTo run the server, you'll need one of the following: [Uvicorn](https://www.uvicorn.org/), [Daphne](https://github.com/django/daphne), or [Hypercorn](https://gitlab.com/pgjones/hypercorn). If your goal is to support both HTTP/1.1 and HTTP/2, you should opt for either Daphne or Hypercorn. Additionally, to test the server, you might need a client command, such as [buf](https://buf.build/docs/installation).\n\n\n## Generate and run\n\nUse the protoc plugin to generate connecpy server and client code.\n\n```sh\nprotoc --python_out=./ --pyi_out=./ --connecpy_out=./ ./haberdasher.proto\n```\n\n### Server code (ASGI)\n\n```python\n# service.py\nimport random\n\nfrom connecpy.exceptions import InvalidArgument\nfrom connecpy.context import ServiceContext\n\nfrom haberdasher_pb2 import Hat, Size\n\n\nclass HaberdasherService(object):\n    async def MakeHat(self, req: Size, ctx: ServiceContext) -\u003e Hat:\n        print(\"remaining_time: \", ctx.time_remaining())\n        if req.inches \u003c= 0:\n            raise InvalidArgument(\n                argument=\"inches\", error=\"I can't make a hat that small!\"\n            )\n        response = Hat(\n            size=req.inches,\n            color=random.choice([\"white\", \"black\", \"brown\", \"red\", \"blue\"]),\n        )\n        if random.random() \u003e 0.5:\n            response.name = random.choice(\n                [\"bowler\", \"baseball cap\", \"top hat\", \"derby\"]\n            )\n\n        return response\n```\n\n```python\n# server.py\nfrom connecpy import context\nfrom connecpy.asgi import ConnecpyASGIApp\n\nimport haberdasher_connecpy\nfrom service import HaberdasherService\n\nservice = haberdasher_connecpy.HaberdasherServer(\n    service=HaberdasherService()\n)\napp = ConnecpyASGIApp()\napp.add_service(service)\n```\n\nRun the server with\n```sh\nuvicorn --port=3000 server:app\n```\nor\n\n```sh\ndaphne --port=3000 server:app\n```\n\nor\n\n```sh\nhypercorn --bind :3000 server:app\n```\n\n### Client code (Asyncronous)\n\n```python\n# async_client.py\nimport asyncio\n\nimport httpx\n\nfrom connecpy.context import ClientContext\nfrom connecpy.exceptions import ConnecpyServerException\n\nimport haberdasher_connecpy, haberdasher_pb2\n\n\nserver_url = \"http://localhost:3000\"\ntimeout_s = 5\n\n\nasync def main():\n    session = httpx.AsyncClient(\n        base_url=server_url,\n        timeout=timeout_s,\n    )\n    client = haberdasher_connecpy.AsyncHaberdasherClient(server_url, session=session)\n\n    try:\n        response = await client.MakeHat(\n            ctx=ClientContext(),\n            request=haberdasher_pb2.Size(inches=12),\n            # Optionally provide a session per request\n            # session=session,\n        )\n        if not response.HasField(\"name\"):\n            print(\"We didn't get a name!\")\n        print(response)\n    except ConnecpyServerException as e:\n        print(e.code, e.message, e.to_dict())\n    finally:\n        # Close the session (could also use a context manager)\n        await session.aclose()\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\nExample output :\n```\nsize: 12\ncolor: \"black\"\nname: \"bowler\"\n```\n\n## Client code (Synchronous)\n\n```python\n# client.py\nfrom connecpy.context import ClientContext\nfrom connecpy.exceptions import ConnecpyServerException\n\nimport haberdasher_connecpy, haberdasher_pb2\n\n\nserver_url = \"http://localhost:3000\"\ntimeout_s = 5\n\n\ndef main():\n    client = haberdasher_connecpy.HaberdasherClient(server_url, timeout=timeout_s)\n\n    try:\n        response = client.MakeHat(\n            ctx=ClientContext(),\n            request=haberdasher_pb2.Size(inches=12),\n        )\n        if not response.HasField(\"name\"):\n            print(\"We didn't get a name!\")\n        print(response)\n    except ConnecpyServerException as e:\n        print(e.code, e.message, e.to_dict())\n\n\nif __name__ == \"__main__\":\n    main()\n```\n\n### Other clients\n\nOf course, you can use any HTTP client to make requests to a Connecpy server. For example, commands like `curl` or `buf curl` can be used, as well as HTTP client libraries such as `requests`, `httpx`, `aiohttp`, and others. The examples below use `curl` and `buf curl`.\n\nContent-Type: application/proto, HTTP/1.1\n```sh\nbuf curl --data '{\"inches\": 12}' -v http://localhost:3000/i2y.connecpy.example.Haberdasher/MakeHat --schema ./haberdasher.proto\n```\n\nOn Windows, Content-Type: application/proto, HTTP/1.1\n```sh\nbuf curl --data '{\\\"inches\\\": 12}' -v http://localhost:3000/i2y.connecpy.example.Haberdasher/MakeHat --schema .\\haberdasher.proto\n```\n\nContent-Type: application/proto, HTTP/2\n```sh\nbuf curl --data '{\"inches\": 12}' -v http://localhost:3000/i2y.connecpy.example.Haberdasher/MakeHat --http2-prior-knowledge --schema ./haberdasher.proto\n```\n\nOn Windows, Content-Type: application/proto, HTTP/2\n```sh\nbuf curl --data '{\\\"inches\\\": 12}' -v http://localhost:3000/i2y.connecpy.example.Haberdasher/MakeHat --http2-prior-knowledge --schema .\\haberdasher.proto\n```\n\n\nContent-Type: application/json, HTTP/1.1\n```sh\ncurl -X POST -H \"Content-Type: application/json\" -d '{\"inches\": 12}' -v http://localhost:3000/i2y.connecpy.example.Haberdasher/MakeHat\n```\n\nOn Windows, Content-Type: application/json, HTTP/1.1\n```sh\ncurl -X POST -H \"Content-Type: application/json\" -d '{\\\"inches\\\": 12}' -v http://localhost:3000/i2y.connecpy.example.Haberdasher/MakeHat\n```\n\nContent-Type: application/json, HTTP/2\n```sh\ncurl --http2-prior-knowledge -X POST -H \"Content-Type: application/json\" -d '{\"inches\": 12}' -v http://localhost:3000/i2y.connecpy.example.Haberdasher/MakeHat\n```\n\nOn Windows, Content-Type: application/json, HTTP/2\n```sh\ncurl --http2-prior-knowledge -X POST -H \"Content-Type: application/json\" -d '{\\\"inches\\\": 12}' -v http://localhost:3000/i2y.connecpy.example.Haberdasher/MakeHat\n```\n\n## WSGI Support\n\nConnecpy now provides WSGI support via the `ConnecpyWSGIApp`. This synchronous application adapts our service endpoints to the WSGI specification. It reads requests from the WSGI `environ`, processes POST requests, and returns responses using `start_response`. This enables integration with legacy WSGI servers and middleware.\n\nPlease see the example in the [example directory](example/wsgi_server.py).\n\n## Compression Support\n\nConnecpy supports various compression methods for both GET and POST requests/responses:\n\n- gzip\n- brotli (br)\n- zstandard (zstd)\n- identity (no compression)\n\nFor GET requests, specify the compression method using the `compression` query parameter:\n```sh\ncurl \"http://localhost:3000/service/method?compression=gzip\u0026message=...\"\n```\n\nFor POST requests, use the `Content-Encoding` header:\n```sh\ncurl -H \"Content-Encoding: br\" -d '{\"data\": \"...\"}' http://localhost:3000/service/method\n```\n\nThe compression is handled directly in the request handlers, ensuring consistent behavior across HTTP methods and frameworks (ASGI/WSGI).\n\nWith Connecpy's compression features, you can automatically handle compressed requests and responses. Here are some examples:\n\n### Server-side\n\nThe compression handling is built into both ASGI and WSGI applications. You don't need any additional middleware configuration - it works out of the box!\n\n### Client-side\n\nFor synchronous clients:\n```python\nfrom connecpy.context import ClientContext\n\nclient = HaberdasherClient(server_url)\nresponse = client.MakeHat(\n    ctx=ClientContext(\n        headers={\n            \"Content-Encoding\": \"br\",  # Use Brotli compression for request\n            \"Accept-Encoding\": \"gzip\",  # Accept gzip compressed response\n        }\n    ),\n    request=request_obj,\n)\n```\n\nFor async clients:\n```python\nasync with httpx.AsyncClient() as session:\n    client = AsyncHaberdasherClient(server_url, session=session)\n    response = await client.MakeHat(\n        ctx=ClientContext(),\n        request=request_obj,\n        headers={\n            \"Content-Encoding\": \"zstd\",  # Use Zstandard compression for request\n            \"Accept-Encoding\": \"br\",     # Accept Brotli compressed response\n        },\n    )\n```\n\nUsing GET requests with compression:\n```python\nresponse = client.MakeHat(\n    ctx=ClientContext(),\n    request=request_obj,\n    use_get=True,  # Enable GET request (for methods marked with no_side_effects)\n    params={\n        \"compression\": \"gzip\",  # Use gzip compression for the message\n    }\n)\n```\n\n### CORS Support\n\nConnecpy provides built-in CORS support via the `CORSMiddleware`. By default, it allows all origins and includes necessary Connect Protocol headers:\n\n```python\nfrom connecpy.cors import CORSMiddleware\n\napp = ConnecpyASGIApp()\napp.add_service(service)\napp = CORSMiddleware(app)  # Use default configuration\n```\n\nYou can customize the CORS behavior using `CORSConfig`:\n\n```python\nfrom connecpy.cors import CORSMiddleware, CORSConfig\n\nconfig = CORSConfig(\n    allow_origin=\"https://your-domain.com\",           # Restrict allowed origins\n    allow_methods=(\"POST\", \"GET\", \"OPTIONS\"),         # Customize allowed methods\n    allow_headers=(                                   # Customize allowed headers\n        \"Content-Type\",\n        \"Connect-Protocol-Version\",\n        \"X-Custom-Header\",\n    ),\n    access_control_max_age=3600,                     # Set preflight cache duration\n)\n\napp = CORSMiddleware(app, config=config)\n```\n\nThe middleware handles both preflight requests (OPTIONS) and adds appropriate CORS headers to responses.\n\n## Connect Protocol\n\nConnecpy protoc plugin generates the code based on [Connect Protocl](https://connectrpc.com/docs/protocol) from the `.proto` files.\nCurrently, Connecpy supports only Unary RPCs using the POST HTTP method. Connecpy will support other types of RPCs as well, in the near future.\n\n## Misc\n\n### Server Path Prefix\n\nYou can set server path prefix by passing `server_path_prefix` to `ConnecpyASGIApp` constructor.\n\nThis example sets server path prefix to `/foo/bar`.\n```python\n# server.py\nservice = haberdasher_connecpy.HaberdasherServer(\n    service=HaberdasherService(),\n    server_path_prefix=\"/foo/bar\",\n)\n```\n\n```python\n# async_client.py\nresponse = await client.MakeHat(\n    ctx=ClientContext(),\n    request=haberdasher_pb2.Size(inches=12),\n    server_path_prefix=\"/foo/bar\",\n)\n```\n\n### Interceptor (Server Side)\n\nConnecpyASGIApp supports interceptors. You can add interceptors by passing `interceptors` to `ConnecpyASGIApp` constructor.\nAsyncConnecpyServerInterceptor\n\n```python\n# server.py\nfrom typing import Any, Callable\n\nfrom connecpy import context\nfrom connecpy.asgi import ConnecpyASGIApp\nfrom connecpy.interceptor import AsyncConnecpyServerInterceptor\n\nimport haberdasher_connecpy\nfrom service import HaberdasherService\n\n\nclass MyInterceptor(AsyncConnecpyServerInterceptor):\n    def __init__(self, msg):\n        self._msg = msg\n\n    async def intercept(\n        self,\n        method: Callable,\n        request: Any,\n        ctx: context.ServiceContext,\n        method_name: str,\n    ) -\u003e Any:\n        print(\"intercepting \" + method_name + \" with \" + self._msg)\n        return await method(request, ctx)\n\n\nmy_interceptor_a = MyInterceptor(\"A\")\nmy_interceptor_b = MyInterceptor(\"B\")\n\nservice = haberdasher_connecpy.HaberdasherServer(service=HaberdasherService())\napp = ConnecpyASGIApp(\n    interceptors=(my_interceptor_a, my_interceptor_b),\n)\napp.add_service(service)\n```\n\nBtw, `ConnecpyServerInterceptor`'s `intercept` method has compatible signature as `intercept` method of [grpc_interceptor.server.AsyncServerInterceptor](https://grpc-interceptor.readthedocs.io/en/latest/#async-server-interceptors), so you might be able to convert Connecpy interceptors to gRPC interceptors by just changing the import statement and the parent class.\n\n\n### gRPC Compatibility\nIn Connecpy, unlike connect-go, it is not possible to simultaneously support both gRPC and Connect RPC on the same server and port. In addition to it, Connecpy itself doesn't support gRPC. However, implementing a gRPC server using the same service code used for Connecpy server is feasible, as shown below. This is possible because the type signature of the service class in Connecpy is compatible with type signature gRPC farmework requires.\nThe example below uses [grpc.aio](https://grpc.github.io/grpc/python/grpc_asyncio.html) and there are in [example dicrectory](example/README.md).\n\n\n```python\n# grpc_server.py\nimport asyncio\n\nfrom grpc.aio import server\n\nimport haberdasher_pb2_grpc\n\n# same service.py as the one used in previous server.py\nfrom service import HaberdasherService\n\nhost = \"localhost:50051\"\n\n\nasync def main():\n    s = server()\n    haberdasher_pb2_grpc.add_HaberdasherServicer_to_server(HaberdasherService(), s)\n    bound_port = s.add_insecure_port(host)\n    print(f\"localhost:{bound_port}\")\n    await s.start()\n    await s.wait_for_termination()\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\n```python\n# grpc_client.py\nimport asyncio\n\nfrom grpc.aio import insecure_channel\n\nimport haberdasher_pb2\nimport haberdasher_pb2_grpc\n\n\ntarget = \"localhost:50051\"\n\n\nasync def main():\n    channel = insecure_channel(target)\n    stub = haberdasher_pb2_grpc.HaberdasherStub(channel)\n    request = haberdasher_pb2.Size(inches=12)\n    response = await stub.MakeHat(request)\n    print(response)\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\n### Message Body Length\n\nCurrently, message body length limit is set to 100kb, you can override this by passing `max_receive_message_length` to `ConnecpyASGIApp` constructor.\n\n```python\n# this sets max message length to be 10 bytes\napp = ConnecpyASGIApp(max_receive_message_length=10)\n\n```\n\n## Standing on the shoulders of giants\n\nThe initial version (1.0.0) of this software was created by modifying https://github.com/verloop/twirpy at January 4, 2024, so that it supports Connect Protocol. Therefore, this software is also licensed under Unlicense same as twirpy.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fi2y%2Fconnecpy","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fi2y%2Fconnecpy","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fi2y%2Fconnecpy/lists"}