{"id":21704130,"url":"https://github.com/so1n/rap","last_synced_at":"2025-04-12T15:22:49.571Z","repository":{"id":43552314,"uuid":"285458843","full_name":"so1n/rap","owner":"so1n","description":"rap(par[::-1]) is advanced and fast python async rpc","archived":false,"fork":false,"pushed_at":"2022-11-20T08:12:29.000Z","size":2020,"stargazers_count":19,"open_issues_count":0,"forks_count":3,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-04-09T18:17:53.616Z","etag":null,"topics":["aiorpc","async-rpc","asyncio","python-rpc","rap","rpc"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/so1n.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2020-08-06T03:00:26.000Z","updated_at":"2025-02-22T20:27:27.000Z","dependencies_parsed_at":"2023-01-23T02:00:59.843Z","dependency_job_id":null,"html_url":"https://github.com/so1n/rap","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/so1n%2Frap","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/so1n%2Frap/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/so1n%2Frap/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/so1n%2Frap/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/so1n","download_url":"https://codeload.github.com/so1n/rap/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248586827,"owners_count":21129111,"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":["aiorpc","async-rpc","asyncio","python-rpc","rap","rpc"],"created_at":"2024-11-25T21:43:52.336Z","updated_at":"2025-04-12T15:22:49.536Z","avatar_url":"https://github.com/so1n.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# rap\nrap(par[::-1]) is advanced and fast python async rpc\n\n`rap` achieves very fast communication through `msgpack` and `Python asyncio` and multiplexing transport, while supporting high concurrency.\nImplement the `protobuf` of `Grpc` through Python functions and TypeHint.\n\nNote: The current `rap` API may change significantly in subsequent versions\n\u003e The rap first version feature idea comes from [aiorpc](https://github.com/choleraehyq/aiorpc)\n\n**Warning**\nThere will be an architectural change in version 0.6\n\n\n[中文文档](https://github.com/so1n/rap/blob/master/README_CH.md)\n# 1.Installation\n```Bash\npip install rap\n```\n\n# 2.Quick Start\n\n## Server\n\n```Python\nimport asyncio\nfrom typing import AsyncIterator\n\nfrom rap.server import Server\n\n\ndef sync_sum(a: int, b: int) -\u003e int:\n  return a + b\n\n\nasync def async_sum(a: int, b: int) -\u003e int:\n  await asyncio.sleep(1)  # mock io\n  return a + b\n\n\nasync def async_gen(a: int) -\u003e AsyncIterator[int]:\n  for i in range(a):\n    yield i\n\n\nloop = asyncio.new_event_loop()\nrpc_server = Server()  # init service\n\n# register func\nrpc_server.register(sync_sum)\nrpc_server.register(async_sum)\nrpc_server.register(async_gen)\n\n# run server\nloop.run_until_complete(rpc_server.create_server())\n\ntry:\n  loop.run_forever()\nexcept KeyboardInterrupt:\n  # stop server\n  loop.run_until_complete(rpc_server.shutdown())\n```\n\n## Client\nThe client supports to invoke the service by `invoke_by_name` and `invoke` methods, but this can not fully use the functions of TypeHint, it is recommended to use `@client.register` to register the function and then invoke it.\n\nNote: For `rap.client` there is no distinction between `async def` and `def`, but functions registered with `@client.register` can be used directly by the user, so functions decorated with `@client.register` should be similar to:\n```Python\nasync def demo(): pass\n```\n\nexample:\n\n```Python\nimport asyncio\nfrom typing import AsyncIterator\n\nfrom rap.client import Client\n\nclient: \"Client\" = Client()  # init client\n\n\n# Declare a function with no function. The function name, function type and return type must be the same as the server side function (async def does not differ from def)\ndef sync_sum(a: int, b: int) -\u003e int:\n  pass\n\n\n# The decorated function must be an async def function\n@client.register()\nasync def sync_sum(a: int, b: int) -\u003e int:\n  pass\n\n\n# The decorated function must be the async def function, because the function is a generator syntax, to `yield` instead of `pass`\n@client.register()\nasync def async_gen(a: int) -\u003e AsyncIterator:\n  yield\n\n\nasync def main():\n  client.add_conn(\"localhost\", 9000)\n  await client.start()\n  # Call the invoke method; read the function name and then invoke `invoke_by_name`.\n  print(f\"invoke result: {await client.invoke(sync_sum, {'a': 1, 'b': 2})}\")\n  # Basic calls to rap.client\n  print(f\"raw invoke result: {await client.invoke_by_name('sync_sum', {'a': 1, 'b': 2})}\")\n\n  # Functions registered through `@client.register` can be used directly\n  # await async_sum(1,3) == await client.invoke_by_name('async_sum', 1, 2)\n  # It is recommended to use the @client.register method, which can be used by tools such as IDE to determine whether the parameter type is wrong\n  print(f\"decorator result: {await sync_sum(1, 3)}\")\n  async_gen_result: list = []\n\n  # Example of an asynchronous generator, which by default opens or reuses the current session of the rap (about the session will be mentioned below)\n  async for i in async_gen(10):\n    async_gen_result.append(i)\n  print(f\"async gen result:{async_gen_result}\")\n\n\nasyncio.run(main())\n```\n# 3.Function Introduction\n## 3.1.Register function\nThe server side supports `def` and `async def`, if it is a `def` function, it will be run with multiple threads. When registering, the TypeHints of the function's parameters and return value will be checked, and an error will be reported if the type does not match the type specified by json.\n\nThe server comes with a registration library. If there are duplicate registrations in the same group, an error will be reported. You can use the `group` parameter to define the group to be registered or redefine the name of the registration with the `name` parameter (you also need to specify the corresponding group when the client calls it).\n\nIn addition, you can set `is_private` to True when registering, so that the function can only be called by the local rap.client.\n```Python\nimport asyncio\nfrom typing import AsyncIterator\n\nfrom rap.server import Server\n\n\ndef demo1(a: int, b: int) -\u003e int:\n    return a + b\n\n\nasync def demo2(a: int, b: int) -\u003e int:\n    await asyncio.sleep(1)\n    return a + b\n\n\nasync def demo_gen(a: int) -\u003e AsyncIterator[int]:\n    for i in range(a):\n        yield i\n\n\nserver: Server = Server()\nserver.register(demo1)   # register def func\nserver.register(demo2)   # register async def func\nserver.register(demo_gen)  # register async iterator func\nserver.register(demo2, name='demo2-alias')   # Register with the value of `name`\nserver.register(demo2, group='new-correlation_id')    # Register and set the groups to be registered\nserver.register(demo2, group='root', is_private=True)  # Register and set the correlation_id to be registered, and set it to private\n```\nFor clients, it is recommended to use `client.register` instead of `client.invoke`, `client.invoke_by_name`.\n`client.register` uses Python syntax to define function names, arguments, parameter types, and return value types,\nIt allows the caller to invoke the function as if it were a normal function, and the function can be checked through tools using the TypeHint feature.\nNote: When using `client.register`, be sure to use `async def ... `.\n```Python\nfrom typing import AsyncIterator\nfrom rap.client import Client\n\nclient: Client = Client()\n\n\n# register func\n@client.register()\nasync def demo1(a: int, b: int) -\u003e int: pass\n\n\n# register async iterator fun, replace `pass` with `yield`\n# Since `async for` will make multiple requests to the same transport over time, it will check if the session is enabled and automatically reuse the current session if it is enabled, otherwise it will create a new session and use it.\n@client.register()\nasync def demo_gen(a: int) -\u003e AsyncIterator: yield\n\n\n# Register the general function and set the name to demo2-alias\n@client.register(name='demo2-alias')\nasync def demo2(a: int, b: int) -\u003e int: pass\n\n\n# Register the general function and set the correlation_id to new-correlation_id\n@client.register(group='new-correlation_id')\nasync def demo2(a: int, b: int) -\u003e int: pass\n```\n## 3.2.session\n[example](https://github.com/so1n/rap/tree/master/example/session)\n\n`rap` client support session function, after enabling the session, all requests will only be requested through the current session's transport to the corresponding server, while each request, the session_id in the header will set the current session id, convenient for the server to identify.\n`rap` sessions support explicit and implicit settings, each with its own advantages and disadvantages, without mandatory restrictions.\n\n```Python\nfrom typing import AsyncIterator\n\nfrom rap.client import Client\n\nclient = Client()\n\n\ndef sync_sum(a: int, b: int) -\u003e int:\n  pass\n\n\n@client.register()\nasync def async_sum(a: int, b: int) -\u003e int:\n  pass\n\n\n@client.register()\nasync def async_gen(a: int) -\u003e AsyncIterator[int]:\n  yield\n\n\nasync def no_param_run():\n  # The rap internal implementation uses the session implicitly via the `contextvar` module\n  print(f\"sync result: {await client.invoke(sync_sum, 1, 2)}\")\n  print(f\"async result: {await async_sum(1, 3)}\")\n\n  # The asynchronous generator detects if a session is enabled, and if so, it automatically reuses the current session, otherwise it creates a session\n  async for i in async_gen(10):\n    print(f\"async gen result:{i}\")\n\n\nasync def param_run(session: \"Session\"):\n  # By explicitly passing the session parameters in\n  print(f\"sync result: {await client.invoke(sync_sum, 1, 2, session=session)}\")\n  print(f\"sync result: {await client.invoke_by_name('sync_sum', 1, 2, session=session)}\")\n  # May be a bit unfriendly\n  print(f\"async result: {await async_sum(1, 3, session=session)}\")\n\n  # The asynchronous generator detects if a session is enabled, and if so, it automatically reuses the current session, otherwise it creates a session\n  async for i in async_gen(10):\n    print(f\"async gen result:{i}\")\n\n\nasync def execute(session: \"Session\"):\n  # The best way to invoke a session explicitly, using a method similar to the mysql cursor\n  # execute will automatically recognize the type of invoke\n  print(f\"sync result: {await session.execute(sync_sum, arg_list=[1, 2])}\")\n  print(f\"sync result: {await session.execute('sync_sum', arg_list=[1, 2])}\")\n  print(f\"async result: {await session.execute(async_sum(1, 3))}\")\n\n  # The asynchronous generator detects if a session is enabled, and if so, it automatically reuses the current session, otherwise it creates a session\n  async for i in async_gen(10):\n    print(f\"async gen result:{i}\")\n\n\nasync def run_once():\n  client.add_conn(\"localhost\", 9000)\n  await client.start()\n  # init session\n  async with client.session as s:\n    await no_param_run()\n    await param_run(s)\n    await execute(s)\n  await client.stop()\n```\n## 3.3.channel\n[example](https://github.com/so1n/rap/tree/master/example/channel)\n\nchannel supports client-server interaction in a duplex manner, similar to Http's WebSocket, it should be noted that the channel does not support group settings.\n\nOnly `@client.register` is supported on the client side to register the channel function, which is characterized by a single argument of type `Channel`.\nThe channel will maintain a session and will only communicate with the server via a transport from the time the channel is enabled to the time it is closed.\nTo avoid the use of 'while True', the channel supports the use of 'async for' syntax and the use of 'while await channel.loop()` syntax instead of 'while True\n```Python\nfrom rap.client import Channel, Client\nfrom rap.client.model import Response\n\nclient = Client()\n\n\n@client.register()\nasync def async_channel(channel: Channel) -\u003e None:\n    await channel.write_to_conn(\"hello\")  # send data\n    cnt: int = 0\n    while await channel.loop(cnt \u003c 3):\n        cnt += 1\n        print(await channel.read_body())  # read data\n\n\n@client.register()\nasync def echo_body(channel: Channel) -\u003e None:\n    await channel.write_to_conn(\"hi!\")\n    # Reads data, returns only when data is read, and exits the loop if it receives a signal to close the channel\n    async for body in channel.iter_body():\n        print(f\"body:{body}\")\n        await channel.write_to_conn(body)\n\n\n@client.register()\nasync def echo_response(channel: Channel) -\u003e None:\n    await channel.write_to_conn(\"hi!\")\n    # Read the response data (including header data), and return only if the data is read, or exit the loop if a signal is received to close the channel\n    async for response in channel.iter():\n        response: Response = response  #  help IDE check type....\n        print(f\"response: {response}\")\n        await channel.write_to_conn(response.body)\n```\n## 3.4.ssl\n[example](https://github.com/so1n/rap/tree/master/example/ssl)\n\nDue to the high degree of encapsulation of the `Python asyncio` module, `rap` can be used very easily with ssl\n```bash\n# Quickly generate ssl.crt and ssl.key\nopenssl req -newkey rsa:2048 -nodes -keyout rap_ssl.key -x509 -days 365 -out rap_ssl.crt\n```\nclient.py\n```Python\nfrom rap.client import Client\n\nclient = Client(ssl_crt_path=\"./rap_ssl.crt\")\n```\nserver.py\n```Python\nfrom rap.server import Server\n\nrpc_server = Server(\n    ssl_crt_path=\"./rap_ssl.crt\",\n    ssl_key_path=\"./rap_ssl.key\",\n)\n```\n## 3.5.event\nThe server side supports `start_event` and `stop_event` for event handling before start and after shutdown respectively.\n\n```Python\nfrom rap.server import Server\n\n\nasync def mock_start():\n  print('start event')\n\n\nasync def mock_stop():\n  print('stop event')\n\n\n# example 1\nserver = Server(start_event_list=[mock_start()], stop_event_list=[mock_stop()])\n# example 2\nserver = Server()\nserver.load_before_start_event([mock_start()])\nserver.load_after_stop_event([mock_stop()])\n```\n## 3.6.middleware\n`rap` currently supports 2 types of middleware::\n- Conn middleware: Used when creating transport, such as limiting the total number of links, etc...\n  reference [block.py](https://github.com/so1n/rap/blob/master/rap/server/middleware/conn/block.py),\n  The `dispatch` method will pass in a transport object, and then determine whether to release it according to the rules (return await self.call_next(transport)) or reject it (await transport.close)\n- Message middleware: only supports normal function calls (no support for `Channel`), similar to the use of `starlette` middleware\n  reference [access.py](https://github.com/so1n/rap/blob/master/rap/server/middleware/msg/access.py)\n  Message middleware will pass in 4 parameters: request(current request object), call_id(current invoke id), func(current invoke function), param(current parameter) and request to return call_id and result(function execution result or exception object)\n\nIn addition, the middleware supports `start_event_handle` and `stop_event_handle` methods, which are called when the `Server` starts and shuts down respectively.\n\nexample:\n\n```Python\nfrom rap.server import Server\nfrom rap.server.plugin.middleware import ConnLimitMiddleware\n\nrpc_server = Server()\nrpc_server.load_middleware([ConnLimitMiddleware()])\n```\n## 3.7.processor\nThe `rap` processor is used to handle inbound and outbound traffic, where `on_request` is for inbound traffic and `on_response` is for outbound traffic.\n\nThe methods of `rap.client` and `rap.server` processors are basically the same, `rap.server` supports `start_event_handle` and `stop_event_handle` methods, which are called when `Server` starts and shuts down respectively\n\n[server crypto processor example](https://github.com/so1n/rap/blob/master/rap/server/processor/crypto.py)\n\n[client crypto processor example](https://github.com/so1n/rap/blob/master/rap/client/processor/crypto.py)\n\n\nclient load processor example\n```Python\nfrom rap.client import Client\nfrom rap.client.processor import CryptoProcessor\n\nclient = Client()\nclient.load_processor([CryptoProcessor('key_id', 'xxxxxxxxxxxxxxxx')])\n```\nserver load processor example\n\n```Python\nfrom rap.server import Server\nfrom rap.server.plugin.processor import CryptoProcessor\n\nserver = Server()\nserver.load_processor([CryptoProcessor({'key_id': 'xxxxxxxxxxxxxxxx'})])\n```\n\n# 4.Plugin\nrap supports plug-in functionality through `middleware` and `processor`, `middleware` only supports the server side, `processor` supports the client and server side\n\n## 4.1.Encrypted transmission\nEncrypted transmission only encrypts the request and response body content, not the header etc. While encrypting, the nonce parameter is added to prevent replay, and the timestamp parameter is added to prevent timeout access.\n\nclient example:\n```Python\nfrom rap.client import Client\nfrom rap.client.processor import CryptoProcessor\n\nclient = Client()\n# The first parameter is the id of the secret key, the server determines which secret key is used for the current request by the id of the secret key\n# The second parameter is the key of the secret key, currently only support the length of 16 bits of the secret key\n# timeout: Requests that exceed the timeout value compared to the current timestamp will be discarded\n# interval: Clear the nonce interval, the shorter the interval, the more frequent the execution, the greater the useless work, the longer the interval, the more likely to occupy memory, the recommended value is the timeout value is 2 times\nclient.load_processor([CryptoProcessor(\"demo_id\", \"xxxxxxxxxxxxxxxx\", timeout=60, interval=120)])\n```\nserver example:\n\n```Python\nfrom rap.server import Server\nfrom rap.server.plugin.processor import CryptoProcessor\n\nserver = Server()\n# The first parameter is the secret key key-value pair, key is the secret key id, value is the secret key\n# timeout: Requests that exceed the timeout value compared to the current timestamp will be discarded\n# nonce_timeout: The expiration time of nonce, the recommended setting is greater than timeout\nserver.load_processor([CryptoProcessor({\"demo_id\": \"xxxxxxxxxxxxxxxx\"}, timeout=60, nonce_timeout=120)])\n```\n## 4.2. Limit the maximum number of transport\nServer-side use only, you can limit the maximum number of links on the server side, more than the set value will not handle new requests\n\n```Python\nfrom rap.server import Server\nfrom rap.server.plugin.middleware import ConnLimitMiddleware, IpMaxConnMiddleware\n\nserver = Server()\nserver.load_middleware(\n    [\n        # max_conn: Current maximum number of transport\n        # block_timeout: Access ban time after exceeding the maximum number of transport\n        ConnLimitMiddleware(max_conn=100, block_time=60),\n        # ip_max_conn: Maximum number of transport per ip\n        # timeout: The maximum statistics time for each ip, after the time no new requests come in, the relevant statistics will be cleared\n        IpMaxConnMiddleware(ip_max_conn=10, timeout=60),\n    ]\n)\n```\n## 4.3.Limit ip access\nSupport restrict single ip or whole segment ip, support both whitelist and blacklist mode, if whitelist is enabled, blacklist mode is disabled by default\n\n```Python\nfrom rap.server import Server\nfrom rap.server.plugin.middleware import IpFilterMiddleware\n\nserver = Server()\n# allow_ip_list: whitelist, support network segment ip, if filled with allow_ip_list, black_ip_list will be invalid\n# black_ip_list: blacklist, support network segment ip\nserver.load_middleware([IpFilterMiddleware(allow_ip_list=['192.168.0.0/31'], block_ip_list=['192.168.0.2'])])\n```\n# 5.Advanced Features\n**TODO**, This feature is not yet implemented\n\n# 6.Protocol Design\n**TODO**, Document is being edited\n\n# 7.Introduction to the rap transport\n**TODO**, Document is being edited\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fso1n%2Frap","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fso1n%2Frap","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fso1n%2Frap/lists"}