Package: pybotx-smartapp-rpc
BotX-SmartApp-RPCБиблиотека, позволяющая писать смартаппы, используянаш JSONRPC-like протоколУстановкаИспользуяpoetry:poetryaddpybotx-smartapp-rpcДобавление RPC методовСоздайте класс для входящих аргументов:frompybotx_smartap_rpcimportRPCArgsBaseModel...classSumArgs(RPCArgsBaseModel):a:intb:intСоздайте RPC метод:frompybotx_smartapp_rpcimportSmartApp,RPCRouter,RPCResultResponse...rpc=RPCRouter()...@rpc.method("sum")asyncdefsum(smartapp:SmartApp,rpc_arguments:SumArgs)->RPCResultResponse[int]:returnRPCResultResponse(result=rpc_arguments.a+rpc_arguments.b)# Так же у метода может не быть аргументов:@rpc.method("answer")asyncdefanswer(smartapp:SmartApp)->RPCResultResponse[int]:returnRPCResultResponse(result=42)Создайте экземплярSmartAppRPCи подключите роутер из прошлого пункта:frompybotx_smartapp_rpcimportSmartAppRPCfromanywhereimportmethods...smartapp=SmartAppRPC(routers=[methods.rpc])Сделайте хендлер дляsmartapp_eventи вызывайте в нем хендлер библиотеки@collector.smartapp_eventasyncdefhandle_smartapp_event(event:SmartAppEvent,bot:Bot)->None:awaitsmartapp.handle_smartapp_event(event,bot)Продвинутая работа с библиотекойВRPCResultResponseможно передаватьbotx.Fileфайлы.@rpc.method("get-pdf")asyncdefget_pdf(smartapp:SmartApp,rpc_arguments:GetPDFArgs)->RPCResultResponse[None]:...returnRPCResultResponse(result=None,files=[...])ВSmartAppRPC,RPCRouterиRPCRouter.methodможно передать мидлвари, сначала будут вызваны мидлвари приложения, затем мидлвари роутера и в конце мидлвари метода.smartapp=SmartAppRPC(...,middlewares=[...])...rpc=RPCRouter(middlewares=[...])...@rpc.method("sum",middlewares=[...])RPCArgsBaseModelэто алиас дляpydantic.BaseModel, вы можете использовать все возможности исходного класса.fromuuidimportUUID...classDelUserArgs(RPCArgsBaseModel):# pydantic сериализует входящую строку в UUIDuser_huid:UUIDЧерез объектsmartapp, передаваемый в хендлер можно получить доступ кeventиbot....@rpc.method("del-user")asyncdefdel_user(smartapp:SmartApp,rpc_arguments:DelUserArgs)->RPCResultResponse[None]:awaitsmartapp.bot.send_message(body="Done",bot_id=smartapp.event.bot.id,chat_id=smartapp.event.chat.id,)...Используя методsmartapp.send_eventможно отправлять RPC ивенты сref: null.Это может пригодиться при необходимости отправки уведомления не в ответ на RPC запрос.@rpc.method("notify-me")asyncdefnotify_me(smartapp:SmartApp,rpc_arguments:NotifyMeArgs)->RPCResultResponse[None]:...awaitsmartapp.send_event("notified",files=[notify_file])...Используя методsmartapp.send_pushилиsmartapp.send_custom_pushможно отправлять пуш уведомлений на клиент.
И обновлять счетчик уведомлений на икноке смартапа.@rpc.method("notify-me")asyncdefnotify_me(smartapp:SmartApp,rpc_arguments:NotifyMeArgs)->RPCResultResponse[None]:awaitsmartapp.send_push(42,"You have 42 new emails!")...В мидлварях можно создавать новые объекты вsmartapp.state, чтобы потом использовать их в хендлерах.asyncdefuser_middleware(smartapp:SmartApp,rpc_arguments:RPCArgsBaseModel,call_next:Callable)->RPCResponse[User]:smartapp.state.user=awaitUser.get(smartapp.message.user_huid)returnawaitcall_next(smartapp,rpc_arguments)@rpc.method("get-user-fullname")asyncdefget_user_fullname(smartapp:SmartApp)->RPCResultResponse[str]:returnRPCResultResponse(result=smartapp.state.user.fullname)Можно выбрасывать пользовательские RPC ошибки, которые будут отправлены как ответ на RPC запрос.frompybotx_smartapp_rpcimportRPCErrorExc,RPCErrorclassCustomError(RPCError):id="CUSTOM_ERROR"reason="It's error reason"...@rpc.method("return-error")asyncdefreturn_error(smartapp:SmartApp,rpc_arguments:RaiseOneErrorArgs)->None:# one errorraiseRPCErrorExc(CustomError(meta={"args":rpc_arguments.dict()},))# or list of errorsraiseRPCErrorExc([CustomError(meta={"args":rpc_arguments.dict()},),RPCError(reason="It's one more error reason",id="CUSTOM_ERROR_NUMBER_TWO",meta={"args":rpc_arguments.dict()},)])Можно добавить хендлер на определенный тип исключений. В него будут отправлять исключения того же и дочерних классов.
ХендлеробязанвозвращатьRPCErrorResponse, ошибки из которого будут отправлены источнику запроса.frompybotx_smartapp_rpcimportSmartAppRPC,RPCErrorResponse...asyncdefkey_error_handler(exc:KeyError,smartapp:SmartApp)->RPCErrorResponse:key=exc.args[0]returnRPCErrorResponse(errors=[RPCError(reason=f"Key{key}not found.",id="KEY_ERROR",meta={"key":key},),])smartapp=SmartAppRPC(...,exception_handlers={KeyError:key_error_handler})Swagger documentationМожно подключить rpc роутеры к авто генерируемой документации FastAPI и использовать
документацию в Swagger. Для этого необходимо переопределить функцию для генерации
OpenAPI схемы:fromfastapiimportFastAPIapplication=FastAPI()defget_custom_openapi():returncustom_openapi(title="Smartapp API",version="0.1.0",fastapi_routes=application.routes,rpc_router=smartapp.router,openapi_version="3.0.2",)application.openapi=get_custom_openapiПример функцииcustom_openapi:fromfastapi.encodersimportjsonable_encoderfromfastapi.openapi.modelsimportOpenAPIfromfastapi.openapi.utilsimportget_openapifrompybotx_smartapp_rpcimportRPCRouterfrompybotx_smartapp_rpc.openapi_utilsimport*frompydantic.schemaimportget_model_name_mapfromstarlette.routingimportBaseRoutedefcustom_openapi(*,title:str,version:str,fastapi_routes:Sequence[BaseRoute],rpc_router:RPCRouter,**kwargs:Any,)->Dict[str,Any]:openapi_dict=get_openapi(title=title,version=version,routes=fastapi_routes,**kwargs,)paths:Dict[str,Dict[str,Any]]={}flat_rpc_models=get_rpc_flat_models_from_routes(rpc_router)rpc_model_name_map=get_model_name_map(flat_rpc_models)rpc_definitions=get_rpc_model_definitions(flat_models=flat_rpc_models,model_name_map=rpc_model_name_map)formethod_nameinrpc_router.rpc_methods.keys():ifnotrpc_router.rpc_methods[method_name].include_in_schema:continuepath=get_rpc_openapi_path(# type: ignoremethod_name=method_name,route=rpc_router.rpc_methods[method_name],model_name_map=rpc_model_name_map,)ifpath:paths.setdefault(f"/{method_name}",{}).update(path)ifrpc_definitions:openapi_dict.setdefault("components",{}).setdefault("schemas",{}).update({k:rpc_definitions[k]forkinsorted(rpc_definitions)})openapi_dict.setdefault("paths",{}).update(paths)returnjsonable_encoder(OpenAPI(**openapi_dict),by_alias=True,exclude_none=True)Возможности RPC SwaggerМожно добавлять теги к запросам, анaлогично FastAPI.rpc=RPCRouter(tags=["RPC"])@rpc.method("documented-method",tags=["docs"])asyncdefdocs(smartapp:SmartApp,rpc_arguments:DocumentedArgs)->RPCResultResponse[DocumentedResponse]:"""Desctiption of this method."""...Можно переопределять pydantic модель успешного ответа.@rpc.method("method",return_type=Response)asyncdefmethod(smartapp:SmartApp,rpc_arguments:MethodArgs)->RPCResultResponse[int]:...Можно исключать некоторые методы из документации.rpc=RPCRouter(include_in_schema=False)@rpc.method("_hidden_method",include_in_schema=False)asyncdefhidden_method(smartapp:SmartApp)->RPCResultResponse[int]:...Можно определять пользовательские ошибки.frompybotx_smartapp_rpcimportRPCError,RPCErrorExcclassMeta(BaseModel):user_id:intusername:strclassUsernotFoundError(RPCError):"""Error description for swagger."""id="UserNotFound"reason="User not found in db"meta:Meta@rpc.method("method-with_error",errors=[UsernotFoundError])asyncdefget_user(smartapp:SmartApp,rpc_arguments:UserArgs)->RPCResultResponse[User]:...raiseRPCErrorExc(UsernotFoundError(meta={"user_id":1,"username":"test"}))