{"id":25042434,"url":"https://github.com/michael-sulyak/aiohttp-rpc","last_synced_at":"2025-04-09T13:09:36.894Z","repository":{"id":44096679,"uuid":"266488469","full_name":"michael-sulyak/aiohttp-rpc","owner":"michael-sulyak","description":"A simple JSON-RPC implementation for aiohttp","archived":false,"fork":false,"pushed_at":"2025-02-28T12:02:02.000Z","size":411,"stargazers_count":26,"open_issues_count":0,"forks_count":7,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-04-02T11:08:01.665Z","etag":null,"topics":["aiohttp","async","json-rpc","python","python3","rpc","rpc-library","websocket"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/michael-sulyak.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.txt","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":"2020-05-24T07:08:00.000Z","updated_at":"2025-02-28T14:01:57.000Z","dependencies_parsed_at":"2024-06-21T02:36:41.024Z","dependency_job_id":"e164ccc3-e04e-4c5b-9727-ea44b7334018","html_url":"https://github.com/michael-sulyak/aiohttp-rpc","commit_stats":{"total_commits":174,"total_committers":6,"mean_commits":29.0,"dds":"0.45977011494252873","last_synced_commit":"690a84d836921e9e7dd515f271f4c0d6d725da82"},"previous_names":["expert-m/aiohttp-rpc"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/michael-sulyak%2Faiohttp-rpc","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/michael-sulyak%2Faiohttp-rpc/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/michael-sulyak%2Faiohttp-rpc/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/michael-sulyak%2Faiohttp-rpc/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/michael-sulyak","download_url":"https://codeload.github.com/michael-sulyak/aiohttp-rpc/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248045245,"owners_count":21038554,"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":["aiohttp","async","json-rpc","python","python3","rpc","rpc-library","websocket"],"created_at":"2025-02-06T04:46:15.646Z","updated_at":"2025-04-09T13:09:36.874Z","avatar_url":"https://github.com/michael-sulyak.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# aiohttp-rpc\n\n[![PyPI](https://img.shields.io/pypi/v/aiohttp-rpc.svg?style=flat)](https://pypi.org/project/aiohttp-rpc/)\n[![PyPI - Python Version](https://img.shields.io/badge/python-3.9%20%7C%203.10%20%7C%203.11%20%7C%203.12%20%7C%203.13-blue?style=flat)](https://www.python.org/downloads/release/python-380/)\n[![Scrutinizer Code Quality](https://img.shields.io/scrutinizer/g/expert-m/aiohttp-rpc.svg?style=flat)](https://scrutinizer-ci.com/g/expert-m/aiohttp-rpc/?branch=master)\n[![GitHub Issues](https://img.shields.io/github/issues/expert-m/aiohttp-rpc.svg?style=flat)](https://github.com/expert-m/aiohttp-rpc/issues)\n[![Gitter](https://img.shields.io/gitter/room/aiohttp-rpc/Lobby)](https://gitter.im/aiohttp-rpc/Lobby)\n[![License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat)](https://opensource.org/licenses/MIT)\n\n\u003e A library for a simple integration of the [JSON-RPC 2.0 protocol](https://www.jsonrpc.org/specification) to a Python application using [aiohttp](https://github.com/aio-libs/aiohttp).\nThe motivation is to provide a simple, fast and reliable way to integrate the JSON-RPC 2.0 protocol into your application on the server and/or client side.\n\u003cbr/\u003e\u003cbr/\u003e\n\u003eThe library has only one dependency:\n\u003e* **[aiohttp](https://github.com/aio-libs/aiohttp)** - Async http client/server framework\n\n## Table Of Contents\n- **[Installation](#installation)**\n    - **[pip](#pip)**\n- **[Usage](#usage)**\n  - **[HTTP Server Example](#http-server-example)**\n  - **[HTTP Client Example](#http-client-example)**\n- [Integration](#Integration)\n- [Middleware](#middleware)\n- [WebSockets](#websockets)\n  - [WS Server Example](#ws-server-example)\n  - [WS Client Example](#ws-client-example)\n- [API Reference](#api-reference)\n- [More examples](#more-examples)\n- [License](#license)\n\n## Installation\n\n#### pip\n```sh\npip install aiohttp-rpc\n```\n\n## Usage\n\n### HTTP Server Example\n\n```python3\nfrom aiohttp import web\nimport aiohttp_rpc\n\n\ndef echo(*args, **kwargs):\n    return {\n        'args': args,\n        'kwargs': kwargs,\n    }\n\n# If the function has rpc_request in arguments, then it is automatically passed\nasync def ping(rpc_request):\n    return 'pong'\n\n\nif __name__ == '__main__':\n    aiohttp_rpc.rpc_server.add_methods([\n        ping,\n        echo,\n    ])\n\n    app = web.Application()\n    app.router.add_routes([\n        web.post('/rpc', aiohttp_rpc.rpc_server.handle_http_request),\n    ])\n    web.run_app(app, host='0.0.0.0', port=8080)\n```\n\n### HTTP Client Example\n```python3\nimport aiohttp_rpc\nimport asyncio\n\nasync def run():\n    async with aiohttp_rpc.JsonRpcClient('http://0.0.0.0:8080/rpc') as rpc:\n        print('#1', await rpc.ping())\n        print('#2', await rpc.echo('one', 'two'))\n        print('#3', await rpc.call('echo', three='3'))\n        print('#4', await rpc.notify('echo', 123))\n        print('#5', await rpc.get_methods())\n        print('#6', await rpc.batch([\n            ['echo', 2], \n            'echo2',\n            'ping',\n        ]))\n\nloop = asyncio.get_event_loop()\nloop.run_until_complete(run())\n```\n\nThis prints:\n```text\n#1 pong\n#2 {'args': ['one', 'two'], 'kwargs': {}}\n#3 {'args': [], 'kwargs': {'three': '3'}}\n#4 None\n#5 {'get_method': {'doc': None, 'args': ['name'], 'kwargs': []}, 'get_methods': {'doc': None, 'args': [], 'kwargs': []}, 'ping': {'doc': None, 'args': ['rpc_request'], 'kwargs': []}, 'echo': {'doc': None, 'args': [], 'kwargs': []}}\n#6 ({'args': [2], 'kwargs': {}}, JsonRpcError(-32601, 'The method does not exist / is not available.'), 'pong')\n```\n\n[back to top](#table-of-contents)\n\n---\n\n\u003cp align=\"center\"\u003e\u003cb\u003e↑ This is enough to start :sunglasses: ↑\u003c/b\u003e\u003c/p\u003e\n\n---\n\n\n## Integration\n\nThe purpose of this library is to simplify life, and not vice versa.\nAnd so, when you start adding existing functions, some problems may arise.\n\nExisting functions can return objects that are not serialized, but this is easy to fix.\nYou can write own `json_serialize`:\n```python3\nfrom aiohttp import web\nimport aiohttp_rpc\nimport uuid\nimport json\nfrom dataclasses import dataclass\nfrom functools import partial\n\n@dataclass\nclass User:  # The object that is not serializable.\n    uuid: uuid.UUID\n    username: str = 'mike'\n    email: str = 'some@mail.com'\n\nasync def get_user_by_uuid(user_uuid) -\u003e User:\n    # Some function which returns not serializable object.\n    # For example, data may be taken from a database.\n    return User(uuid=uuid.UUID(user_uuid))\n\n\ndef json_serialize_unknown_value(value):\n    if isinstance(value, User):\n        return {\n            'uuid': str(value.uuid),\n            'username': value.username,\n            'email': value.email,\n        }\n\n    return repr(value)\n\nif __name__ == '__main__':\n    rpc_server = aiohttp_rpc.JsonRpcServer(\n        json_serialize=partial(json.dumps, default=json_serialize_unknown_value),\n    )\n    rpc_server.add_method(get_user_by_uuid)\n    \n    app = web.Application()\n    app.router.add_routes([\n        web.post('/rpc', rpc_server.handle_http_request),\n    ])\n    web.run_app(app, host='0.0.0.0', port=8080)\n...\n\n\"\"\"\nExample of response:\n{\n    \"id\": 1,\n    \"jsonrpc\": \"2.0\",\n    \"result\": {\n        \"uuid\": \"600d57b3-dda8-43d0-af79-3e81dbb344fa\",\n        \"username\": \"mike\",\n        \"email\": \"some@mail.com\"\n    }\n}\n\"\"\"\n```\n\nBut you can go further.\nIf you want to use functions that accept custom types,\nthen you can do something like this:\n```python3\n# The function (RPC method) that takes a custom type.\ndef generate_user_token(user: User):\n    return f'token-{str(user.uuid).split(\"-\")[0]}'\n\nasync def replace_type(data):\n    if not isinstance(data, dict) or '__type__' not in data:\n        return data\n\n    if data['__type__'] == 'user':\n        return await get_user_by_uuid(data['uuid'])\n\n    raise aiohttp_rpc.errors.InvalidParams\n\n# The middleware that converts types\nasync def type_conversion_middleware(request, handler):\n    request.set_args_and_kwargs(\n        args=[await replace_type(arg) for arg in request.args],\n        kwargs={key: await replace_type(value) for key, value in request.kwargs.items()},\n    )\n    return await handler(request)\n\n\nrpc_server = aiohttp_rpc.JsonRpcServer(middlewares=[\n    aiohttp_rpc.middlewares.exception_middleware,\n    aiohttp_rpc.middlewares.extra_args_middleware,\n    type_conversion_middleware,\n])\n\n\"\"\"\nRequest:\n{\n    \"id\": 1234,\n    \"jsonrpc\": \"2.0\",\n    \"method\": \"generate_user_token\",\n    \"params\": [{\"__type__\": \"user\", \"uuid\": \"600d57b3-dda8-43d0-af79-3e81dbb344fa\"}]\n}\n\nResponse:\n{\n    \"id\": 1234,\n    \"jsonrpc\": \"2.0\",\n    \"result\": \"token-600d57b3\"\n}\n\"\"\"\n```\n\n[Middleware](#middleware) allows you to replace arguments, responses, and more.\n\nIf you want to add permission checking for each method,\nthen you can override the class `JsonRpcMethod` or use [middleware](#middleware).\n\n[back to top](#table-of-contents)\n\n---\n\n\n## Middleware\n\nMiddleware is used for [RPC Request / RPC Response](#protocol) processing.\nIt has a similar interface as [aiohttp middleware](https://docs.aiohttp.org/en/stable/web_advanced.html#middlewares).\n\n```python3\nimport aiohttp_rpc\nimport typing\n\nasync def simple_middleware(request: aiohttp_rpc.JsonRpcRequest, handler: typing.Callable) -\u003e aiohttp_rpc.JsonRpcResponse:\n    # Code to be executed for each RPC request before\n    # the method (and later middleware) are called.\n\n    response = await handler(request)\n\n    # Code to be executed for each RPC request / RPC response after\n    # the method is called.\n\n    return response\n\nrpc_server = aiohttp_rpc.JsonRpcServer(middlewares=[\n     aiohttp_rpc.middlewares.exception_middleware,\n     simple_middleware,\n])\n```\n\nOr use [aiohttp middlewares](https://docs.aiohttp.org/en/stable/web_advanced.html#middlewares) to process `web.Request`/`web.Response`.\n\n[back to top](#table-of-contents)\n\n---\n\n\n## WebSockets\n\n### WS Server Example\n\n```python3\nfrom aiohttp import web\nimport aiohttp_rpc\n\n\nasync def ping(rpc_request):\n    return 'pong'\n\n\nif __name__ == '__main__':\n    rpc_server = aiohttp_rpc.WsJsonRpcServer(\n        middlewares=aiohttp_rpc.middlewares.DEFAULT_MIDDLEWARES,\n    )\n    rpc_server.add_method(ping)\n\n    app = web.Application()\n    app.router.add_routes([\n        web.get('/rpc', rpc_server.handle_http_request),\n    ])\n    app.on_shutdown.append(rpc_server.on_shutdown)\n    web.run_app(app, host='0.0.0.0', port=8080)\n```\n\n### WS Client Example\n```python3\nimport aiohttp_rpc\nimport asyncio\n\nasync def run():\n    async with aiohttp_rpc.WsJsonRpcClient('http://0.0.0.0:8080/rpc') as rpc:\n        print(await rpc.ping())\n        print(await rpc.notify('ping'))\n        print(await rpc.batch([\n            ['echo', 2], \n            'echo2',\n            'ping',\n        ]))\n\nloop = asyncio.get_event_loop()\nloop.run_until_complete(run())\n```\n\n[back to top](#table-of-contents)\n\n---\n\n## API Reference\n\n\n### `server`\n  * `class JsonRpcServer(BaseJsonRpcServer)`\n    * `def __init__(self, *, json_serialize=json_serialize, middlewares=(), methods=None)`\n    * `def add_method(self, method, *, replace=False) -\u003e JsonRpcMethod`\n    * `def add_methods(self, methods, replace=False) -\u003e typing.List[JsonRpcMethod]`\n    * `def get_method(self, name) -\u003e Optional[Mapping]`\n    * `def get_methods(self) -\u003e Mapping[str, Mapping]`\n    * `async def handle_http_request(self, http_request: web.Request) -\u003e web.Response`\n \n  * `class WsJsonRpcServer(BaseJsonRpcServer)`\n  * `rpc_server: JsonRpcServer`\n  \n\n### `client`\n  * `class JsonRpcClient(BaseJsonRpcClient)`\n    * `async def connect(self)`\n    * `async def disconnect(self)`\n    * `async def call(self, method: str, *args, **kwargs)`\n    * `async def notify(self, method: str, *args, **kwargs)`\n    * `async def batch(self, methods])`\n    * `async def batch_notify(self, methods)`\n  \n  * `class WsJsonRpcClient(BaseJsonRpcClient)`\n\n### `protocol`\n  * `class JsonRpcRequest`\n    * `id: Union[int, str, None]`\n    * `method: str`\n    * `jsonrpc: str`\n    * `extra_args: MutableMapping`\n    * `context: MutableMapping`\n    * `params: Any`\n    * `args: Optional[Sequence]`\n    * `kwargs: Optional[Mapping]`\n    * `is_notification: bool`\n    \n  * `class JsonRpcResponse`\n    * `id: Union[int, str, None]`\n    * `jsonrpc: str`\n    * `result: Any`\n    * `error: Optional[JsonRpcError]`\n    * `context: MutableMapping`\n    \n  * `class JsonRpcMethod(BaseJsonRpcMethod)`\n    * `def __init__(self, func, *, name=None, add_extra_args=True, prepare_result=None)`\n  \n  * `class JsonRpcUnlinkedResults`\n\n  * `class JsonRpcDuplicatedResults`\n\n### `decorators`\n  * `def rpc_method(*, rpc_server=default_rpc_server, name=None, add_extra_args=True)`\n\n### `errors`\n  * `class JsonRpcError(RuntimeError)`\n  * `class ServerError(JsonRpcError)`\n  * `class ParseError(JsonRpcError)`\n  * `class InvalidRequest(JsonRpcError)`\n  * `class MethodNotFound(JsonRpcError)`\n  * `class InvalidParams(JsonRpcError)`\n  * `class InternalError(JsonRpcError)`\n  * `DEFAULT_KNOWN_ERRORS`\n  \n### `middlewares`\n  * `async def extra_args_middleware(request, handler)`\n  * `async def exception_middleware(request, handler)`\n  * `DEFAULT_MIDDLEWARES`\n\n### `utils`\n  * `def json_serialize(*args, **kwargs)`\n\n### `constants`\n  * `NOTHING`\n  * `VERSION_2_0`\n\n[back to top](#table-of-contents)\n\n---\n\n\n## More examples\n\n**The library allows you to add methods in many ways:**\n```python3\nimport aiohttp_rpc\n\ndef ping_1(rpc_request): return 'pong 1'\ndef ping_2(rpc_request): return 'pong 2'\ndef ping_3(rpc_request): return 'pong 3'\n\nrpc_server = aiohttp_rpc.JsonRpcServer()\nrpc_server.add_method(ping_1)  # 'ping_1'\nrpc_server.add_method(aiohttp_rpc.JsonRpcMethod(ping_2))  # 'ping_2'\nrpc_server.add_method(aiohttp_rpc.JsonRpcMethod(ping_3, name='third_ping'))  # 'third_ping'\nrpc_server.add_methods([ping_3])  # 'ping_3'\n\n# Replace method\nrpc_server.add_method(ping_1, replace=True)  # 'ping_1'\nrpc_server.add_methods([ping_1, ping_2], replace=True)  # 'ping_1', 'ping_2'\n```\n\n**Example with built-in functions:**\n```python3\n# Server\nimport aiohttp_rpc\n\nrpc_server = aiohttp_rpc.JsonRpcServer(middlewares=[aiohttp_rpc.middlewares.extra_args_middleware])\nrpc_server.add_method(sum)\nrpc_server.add_method(aiohttp_rpc.JsonRpcMethod(zip, prepare_result=list))\n...\n\n# Client\nasync with aiohttp_rpc.JsonRpcClient('/rpc') as rpc:\n    assert await rpc.sum([1, 2, 3]) == 6\n    assert await rpc.zip(['a', 'b'], [1, 2]) == [['a', 1], ['b', 2]]\n```\n\n**Example with the decorator:**\n```python3\nimport aiohttp_rpc\nfrom aiohttp import web\n\n@aiohttp_rpc.rpc_method()\ndef echo(*args, **kwargs):\n    return {\n        'args': args,\n        'kwargs': kwargs,\n    }\n\nif __name__ == '__main__':\n    app = web.Application()\n    app.router.add_routes([\n        web.post('/rpc', aiohttp_rpc.rpc_server.handle_http_request),\n    ])\n    web.run_app(app, host='0.0.0.0', port=8080)\n```\n\n**It is possible to pass params into aiohttp request via `direct_call`/`direct_batch`:**\n```python3\nimport aiohttp_rpc\n\njsonrpc_request = aiohttp_rpc.JsonRpcRequest(method_name='test', params={'test_value': 1})\nasync with aiohttp_rpc.JsonRpcClient('/rpc') as rpc:\n    await rpc.direct_call(jsonrpc_request, headers={'My-Customer-Header': 'custom value'}, timeout=10)\n```\n\n[back to top](#table-of-contents)\n\n---\n\n\n## License\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmichael-sulyak%2Faiohttp-rpc","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmichael-sulyak%2Faiohttp-rpc","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmichael-sulyak%2Faiohttp-rpc/lists"}