{"id":29925497,"url":"https://github.com/expressapp/pybotx-smartapp-rpc","last_synced_at":"2025-08-02T11:39:11.670Z","repository":{"id":38302439,"uuid":"473600866","full_name":"ExpressApp/pybotx-smartapp-rpc","owner":"ExpressApp","description":null,"archived":false,"fork":false,"pushed_at":"2024-07-10T11:18:25.000Z","size":211,"stargazers_count":0,"open_issues_count":1,"forks_count":0,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-05-17T06:42:57.593Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ExpressApp.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":"2022-03-24T12:40:55.000Z","updated_at":"2025-04-03T18:12:04.000Z","dependencies_parsed_at":"2023-02-12T23:46:20.794Z","dependency_job_id":"f95b5856-7ace-4b83-b326-343fefbf6690","html_url":"https://github.com/ExpressApp/pybotx-smartapp-rpc","commit_stats":null,"previous_names":[],"tags_count":26,"template":false,"template_full_name":null,"purl":"pkg:github/ExpressApp/pybotx-smartapp-rpc","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ExpressApp%2Fpybotx-smartapp-rpc","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ExpressApp%2Fpybotx-smartapp-rpc/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ExpressApp%2Fpybotx-smartapp-rpc/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ExpressApp%2Fpybotx-smartapp-rpc/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ExpressApp","download_url":"https://codeload.github.com/ExpressApp/pybotx-smartapp-rpc/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ExpressApp%2Fpybotx-smartapp-rpc/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":268380008,"owners_count":24241168,"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","status":"online","status_checked_at":"2025-08-02T02:00:12.353Z","response_time":74,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"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":[],"created_at":"2025-08-02T11:37:15.299Z","updated_at":"2025-08-02T11:39:11.659Z","avatar_url":"https://github.com/ExpressApp.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# BotX-SmartApp-RPC\nБиблиотека, позволяющая писать смартаппы, используя [наш JSONRPC-like протокол](https://ccsteam.atlassian.net/wiki/spaces/EI/pages/193167368/SmartApp+RPC)\n\n## Установка\nИспользуя `poetry`:\n\n```bash\npoetry add pybotx-smartapp-rpc\n```\n\n## Добавление RPC методов\n1. Создайте класс для входящих аргументов:\n``` python\nfrom pybotx_smartap_rpc import RPCArgsBaseModel\n...\nclass SumArgs(RPCArgsBaseModel):\n    a: int\n    b: int\n```\n2. Создайте RPC метод:\n``` python\nfrom pybotx_smartapp_rpc import SmartApp, RPCRouter, RPCResultResponse\n...\nrpc = RPCRouter()\n...\n@rpc.method(\"sum\")\nasync def sum(\n    smartapp: SmartApp, rpc_arguments: SumArgs\n) -\u003e RPCResultResponse[int]:\n    return RPCResultResponse(result=rpc_arguments.a + rpc_arguments.b)\n\n# Так же у метода может не быть аргументов:\n@rpc.method(\"answer\")\nasync def answer(smartapp: SmartApp) -\u003e RPCResultResponse[int]:\n    return RPCResultResponse(result=42)\n```\n3. Создайте экземпляр `SmartAppRPC` и подключите роутер из прошлого пункта:\n``` python\nfrom pybotx_smartapp_rpc import SmartAppRPC\n\nfrom anywhere import methods \n...\nsmartapp = SmartAppRPC(routers=[methods.rpc])\n```\n4. Сделайте хендлер для `smartapp_event` и вызывайте в нем хендлер библиотеки:  \n    a. Aсинхронный подход:\n    ``` python\n    @collector.smartapp_event\n    async def handle_smartapp_event(event: SmartAppEvent, bot: Bot) -\u003e None:\n        await smartapp.handle_smartapp_event(event, bot)\n    ```\n   б. Синхронный подход:\n    ``` python\n   from pybotx import SyncSmartAppEventResponsePayload\n   \n   ...\n   \n    @collector.sync_smartapp_event\n    async def handle_sync_smartapp_event(event: SmartAppEvent, bot: Bot) -\u003e SyncSmartAppEventResponsePayload:\n        return await smartapp.handle_sync_smartapp_event(event, bot)\n    ```\n\n## Продвинутая работа с библиотекой\n* В `RPCResultResponse` можно передавать `botx.File` файлы.\n``` python\n@rpc.method(\"get-pdf\")\nasync def get_pdf(\n    smartapp: SmartApp, rpc_arguments: GetPDFArgs\n) -\u003e RPCResultResponse[None]:\n    ...\n    return RPCResultResponse(result=None, files=[...])\n```\n* В `SmartAppRPC`, `RPCRouter` и `RPCRouter.method` можно передать мидлвари, сначала будут вызваны мидлвари приложения, затем мидлвари роутера и в конце мидлвари метода.\n``` python\nsmartapp = SmartAppRPC(..., middlewares=[...])\n...\nrpc = RPCRouter(middlewares=[...])\n...\n@rpc.method(\"sum\", middlewares=[...])\n```\n* `RPCArgsBaseModel` это алиас для `pydantic.BaseModel`, вы можете использовать все возможности исходного класса.\n``` python\nfrom uuid import UUID\n...\nclass DelUserArgs(RPCArgsBaseModel):\n    # pydantic сериализует входящую строку в UUID\n    user_huid: UUID\n```\n* Через объект `smartapp`, передаваемый в хендлер можно получить доступ к `event` и `bot`.\n``` python\n...\n@rpc.method(\"del-user\")\nasync def del_user(\n    smartapp: SmartApp, rpc_arguments: DelUserArgs\n) -\u003e RPCResultResponse[None]:\n    await smartapp.bot.send_message(\n        body=\"Done\",\n        bot_id=smartapp.event.bot.id,\n        chat_id=smartapp.event.chat.id,\n    )\n    ...\n```\n* Используя метод `smartapp.send_event` можно отправлять RPC ивенты с `ref: null`.  \nЭто может пригодиться при необходимости отправки уведомления не в ответ на RPC запрос.\n``` python\n@rpc.method(\"notify-me\")\nasync def notify_me(\n    smartapp: SmartApp, rpc_arguments: NotifyMeArgs\n) -\u003e RPCResultResponse[None]:\n    ...\n    await smartapp.send_event(\"notified\", files=[notify_file])\n    ...\n```\n* Используя метод `smartapp.send_push` или `smartapp.send_custom_push` можно отправлять пуш уведомлений на клиент.\nИ обновлять счетчик уведомлений на икноке смартапа.\n``` python\n@rpc.method(\"notify-me\")\nasync def notify_me(\n    smartapp: SmartApp, rpc_arguments: NotifyMeArgs\n) -\u003e RPCResultResponse[None]:\n    await smartapp.send_push(42, \"You have 42 new emails!\")\n    ...\n```\n* В мидлварях можно создавать новые объекты в `smartapp.state`, чтобы потом использовать их в хендлерах.\n``` python\nasync def user_middleware(smartapp: SmartApp, rpc_arguments: RPCArgsBaseModel, call_next: Callable) -\u003e RPCResponse[User]:\n    smartapp.state.user = await User.get(smartapp.message.user_huid)\n    return await call_next(smartapp, rpc_arguments)\n\n@rpc.method(\"get-user-fullname\")\nasync def get_user_fullname(smartapp: SmartApp) -\u003e RPCResultResponse[str]:\n    return RPCResultResponse(result=smartapp.state.user.fullname)\n```\n* Можно выбрасывать пользовательские RPC ошибки, которые будут отправлены как ответ на RPC запрос.\n``` python\nfrom pybotx_smartapp_rpc import RPCErrorExc, RPCError\n\nclass CustomError(RPCError):\n    id = \"CUSTOM_ERROR\"\n    reason = \"It's error reason\"\n\n...\n@rpc.method(\"return-error\")\nasync def return_error(smartapp: SmartApp, rpc_arguments: RaiseOneErrorArgs) -\u003e None:\n    # one error\n    raise RPCErrorExc(\n        CustomError(\n            meta={\"args\": rpc_arguments.dict()},\n        )\n    )\n    # or list of errors\n    raise RPCErrorExc(\n        [\n            CustomError(\n                meta={\"args\": rpc_arguments.dict()},\n            ),\n            RPCError(\n                reason=\"It's one more error reason\",\n                id=\"CUSTOM_ERROR_NUMBER_TWO\",\n                meta={\"args\": rpc_arguments.dict()},\n            )\n        ]\n    )\n```\n* Можно добавить хендлер на определенный тип исключений. В него будут отправлять исключения того же и дочерних классов.\nХендлер **обязан** возвращать `RPCErrorResponse`, ошибки из которого будут отправлены источнику запроса.\n``` python\nfrom pybotx_smartapp_rpc import SmartAppRPC, RPCErrorResponse\n...\nasync def key_error_handler(exc: KeyError, smartapp: SmartApp) -\u003e RPCErrorResponse:\n    key = exc.args[0]\n    return RPCErrorResponse(\n        errors=[\n            RPCError(\n                reason=f\"Key {key} not found.\",\n                id=\"KEY_ERROR\",\n                meta={\"key\": key},\n            ),\n        ]\n    )\n\nsmartapp = SmartAppRPC(..., exception_handlers={KeyError: key_error_handler})\n```\n\n### Swagger documentation\nМожно подключить rpc роутеры к авто генерируемой документации FastAPI и использовать\nдокументацию в Swagger. Для этого необходимо переопределить функцию для генерации \nOpenAPI схемы:\n```python\nfrom fastapi import FastAPI\n\napplication = FastAPI()\ndef get_custom_openapi():\n    return custom_openapi(\n        title=\"Smartapp API\",\n        version=\"0.1.0\",\n        fastapi_routes=application.routes,\n        rpc_router=smartapp.router,\n        openapi_version=\"3.0.2\",\n    )\n\napplication.openapi = get_custom_openapi\n```\n\nПример функции `custom_openapi`:\n```python\nfrom fastapi.encoders import jsonable_encoder\nfrom fastapi.openapi.models import OpenAPI\nfrom fastapi.openapi.utils import get_openapi\nfrom pybotx_smartapp_rpc import RPCRouter\nfrom pybotx_smartapp_rpc.openapi_utils import *\nfrom pydantic.schema import get_model_name_map\nfrom starlette.routing import BaseRoute\n\n\ndef custom_openapi(\n    *,\n    title: str,\n    version: str,\n    fastapi_routes: Sequence[BaseRoute],\n    rpc_router: RPCRouter,\n    **kwargs: Any,\n) -\u003e Dict[str, Any]:\n    openapi_dict = get_openapi(\n        title=title,\n        version=version,\n        routes=fastapi_routes,\n        **kwargs,\n    )\n\n    paths: Dict[str, Dict[str, Any]] = {}\n\n    flat_rpc_models = get_rpc_flat_models_from_routes(rpc_router)\n    rpc_model_name_map = get_model_name_map(flat_rpc_models)\n    rpc_definitions = get_rpc_model_definitions(\n        flat_models=flat_rpc_models, model_name_map=rpc_model_name_map\n    )\n\n    for method_name in rpc_router.rpc_methods.keys():\n        if not rpc_router.rpc_methods[method_name].include_in_schema:\n            continue\n\n        path = get_rpc_openapi_path(  # type: ignore\n            method_name=method_name,\n            route=rpc_router.rpc_methods[method_name],\n            model_name_map=rpc_model_name_map,\n        )\n        if path:\n            paths.setdefault(f\"/{method_name}\", {}).update(path)\n\n    if rpc_definitions:\n        openapi_dict.setdefault(\"components\", {}).setdefault(\"schemas\", {}).update(\n            {k: rpc_definitions[k] for k in sorted(rpc_definitions)}\n        )\n\n    openapi_dict.setdefault(\"paths\", {}).update(paths)\n\n    return jsonable_encoder(OpenAPI(**openapi_dict), by_alias=True, exclude_none=True)\n```\n### Возможности RPC Swagger\n\n* Можно добавлять теги к запросам, анaлогично FastAPI.\n``` python\nrpc = RPCRouter(tags=[\"RPC\"])\n\n@rpc.method(\"documented-method\", tags=[\"docs\"])\nasync def docs(\n    smartapp: SmartApp, rpc_arguments: DocumentedArgs\n) -\u003e RPCResultResponse[DocumentedResponse]:\n    \"\"\"Desctiption of this method.\"\"\"\n    ...\n```\n* Можно переопределять pydantic модель успешного ответа.\n``` python\n@rpc.method(\"method\", return_type=Response)\nasync def method(\n    smartapp: SmartApp, rpc_arguments: MethodArgs\n) -\u003e RPCResultResponse[int]:\n    ...\n```\n* Можно исключать некоторые методы из документации.\n``` python\nrpc = RPCRouter(include_in_schema=False)\n\n@rpc.method(\"_hidden_method\", include_in_schema=False)\nasync def hidden_method(smartapp: SmartApp) -\u003e RPCResultResponse[int]:\n    ...\n```\n* Можно определять пользовательские ошибки.\n``` python\nfrom pybotx_smartapp_rpc import RPCError, RPCErrorExc\n\nclass Meta(BaseModel):\n    user_id: int\n    username: str\n\n\nclass UsernotFoundError(RPCError):\n    \"\"\"Error description for swagger.\"\"\"\n    id = \"UserNotFound\"\n    reason = \"User not found in db\"\n    meta: Meta\n\n\n@rpc.method(\"method-with_error\", errors=[UsernotFoundError])\nasync def get_user(\n    smartapp: SmartApp, rpc_arguments: UserArgs\n) -\u003e RPCResultResponse[User]:\n    ...\n    raise RPCErrorExc(UsernotFoundError(meta={\"user_id\": 1, \"username\": \"test\"}))\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fexpressapp%2Fpybotx-smartapp-rpc","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fexpressapp%2Fpybotx-smartapp-rpc","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fexpressapp%2Fpybotx-smartapp-rpc/lists"}