diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 5ebebcb..963922e 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,15 @@ Changelog ========= +2.0.1 (2025-10-28) +------------------- + +- pydantic v1 validator/generator bugs fixed. +- aiohttp subapp specification wrong method path fixed. +- dispatcher middleware invocation order fixed. +- documentation fixed. + + 2.0.0 (2025-10-24) ------------------- diff --git a/README.rst b/README.rst index d32176d..db31e0e 100644 --- a/README.rst +++ b/README.rst @@ -634,9 +634,9 @@ and Swagger UI web tool with basic auth: -Specification is available on http://localhost:8080/rpc/api/v1/openapi.json +Specification is available on http://localhost:8080/rpc/api/openapi.json -Web UI is running on http://localhost:8080/rpc/api/v1/swagger/ and http://localhost:8080/rpc/api/v1/redoc/ +Web UI is running on http://localhost:8080/rpc/api/swagger/ and http://localhost:8080/rpc/api/redoc/ Swagger UI: ~~~~~~~~~~~ diff --git a/docs/source/pjrpc/quickstart.rst b/docs/source/pjrpc/quickstart.rst index 305bcfe..41a31d7 100644 --- a/docs/source/pjrpc/quickstart.rst +++ b/docs/source/pjrpc/quickstart.rst @@ -568,9 +568,9 @@ and Swagger UI web tool with basic auth: web.run_app(http_app, host='localhost', port=8080) -Specification is available on http://localhost:8080/rpc/api/v1/openapi.json +Specification is available on http://localhost:8080/rpc/api/openapi.json -Web UI is running on http://localhost:8080/rpc/api/v1/swagger/ and http://localhost:8080/rpc/api/v1/redoc/ +Web UI is running on http://localhost:8080/rpc/api/swagger/ and http://localhost:8080/rpc/api/redoc/ Swagger UI: ~~~~~~~~~~~ diff --git a/docs/source/pjrpc/webui.rst b/docs/source/pjrpc/webui.rst index 11cfd71..b4893bc 100644 --- a/docs/source/pjrpc/webui.rst +++ b/docs/source/pjrpc/webui.rst @@ -234,9 +234,9 @@ using flask web framework: -Specification is available on http://localhost:8080/myapp/api/v1/openapi.json +Specification is available on http://localhost:8080/rpc/api/openapi.json -Web UI is running on http://localhost:8080/myapp/api/v1/ui/ +Web UI is running on http://localhost:8080/rpc/api/v1/swagger/ and http://localhost:8080/rpc/api/v1/redoc/ Swagger UI ~~~~~~~~~~ diff --git a/pjrpc/server/dispatcher.py b/pjrpc/server/dispatcher.py index 157e228..43863e1 100644 --- a/pjrpc/server/dispatcher.py +++ b/pjrpc/server/dispatcher.py @@ -559,7 +559,7 @@ def __init__( self._max_batch_size = max_batch_size self._executor = executor or BasicAsyncExecutor() - self._request_handler = self._wrap_handle_request() + self._rpc_request_handler = self._wrap_handle_rpc_request() async def dispatch(self, request_text: str, context: ContextType) -> Optional[tuple[str, tuple[int, ...]]]: """ @@ -596,12 +596,12 @@ async def dispatch(self, request_text: str, context: ContextType) -> Optional[tu ) else: responses = ( - resp for resp in await self._executor.execute(self._request_handler, request, context) + resp for resp in await self._executor.execute(self._handle_request, request, context) if not isinstance(resp, UnsetType) ) response = self._batch_response(tuple(responses)) else: - response = await self._request_handler(request, context) + response = await self._handle_request(request, context) if not isinstance(response, UnsetType): response_text = self._json_dumper(response.to_json(), cls=self._json_encoder) @@ -617,10 +617,11 @@ def add_middlewares(self, *middlewares: AsyncMiddlewareType[ContextType], before else: self._middlewares = self._middlewares + list(middlewares) - self._request_handler = self._wrap_handle_request() + self._rpc_request_handler = self._wrap_handle_rpc_request() + + def _wrap_handle_rpc_request(self) -> Callable[[Request, ContextType], Awaitable[MaybeSet[Response]]]: + request_handler = self._handle_rpc_request - def _wrap_handle_request(self) -> Callable[[Request, ContextType], Awaitable[MaybeSet[Response]]]: - request_handler = self._handle_request for middleware in reversed(self._middlewares): request_handler = ft.partial(middleware, handler=request_handler) @@ -628,7 +629,7 @@ def _wrap_handle_request(self) -> Callable[[Request, ContextType], Awaitable[May async def _handle_request(self, request: Request, context: ContextType) -> MaybeSet[Response]: try: - return await self._handle_rpc_request(request, context) + return await self._rpc_request_handler(request, context) except pjrpc.exceptions.JsonRpcError as e: logger.info("method execution error %s(%r): %r", request.method, request.params, e) error = e diff --git a/pjrpc/server/integration/aiohttp.py b/pjrpc/server/integration/aiohttp.py index 615f54a..6794d68 100644 --- a/pjrpc/server/integration/aiohttp.py +++ b/pjrpc/server/integration/aiohttp.py @@ -61,6 +61,10 @@ def __init__( self._endpoints: dict[str, AioHttpDispatcher] = {} self._subapps: dict[str, Application] = {} + @property + def prefix(self) -> str: + return self._prefix + @property def http_app(self) -> web.Application: """ @@ -149,7 +153,7 @@ def generate_spec(self, spec: specs.Specification, base_path: str = '', endpoint app_endpoints = self._endpoints for prefix, subapp in self._subapps.items(): for subprefix, dispatcher in subapp.endpoints.items(): - app_endpoints[utils.join_path(prefix, subprefix)] = dispatcher + app_endpoints[utils.join_path(prefix, subapp._prefix, subprefix)] = dispatcher methods = { utils.remove_prefix(dispatcher_endpoint, endpoint): dispatcher.registry.values() diff --git a/pjrpc/server/specs/extractors/pydantic_v1.py b/pjrpc/server/specs/extractors/pydantic_v1.py index 7a558aa..ef74b35 100644 --- a/pjrpc/server/specs/extractors/pydantic_v1.py +++ b/pjrpc/server/specs/extractors/pydantic_v1.py @@ -135,20 +135,19 @@ def extract_response_schema( ) -> tuple[dict[str, Any], dict[str, dict[str, Any]]]: return_model = self._build_result_model(method_name, method) - response_model: type[pd.BaseModel] - error_models = tuple( - pd.create_model( - error.__name__, - __base__=JsonRpcResponseError[JsonRpcError[Literal[error.CODE], Any]], # type: ignore[name-defined] - __config__=pydantic.config.get_config( + error_models: list[type[pd.BaseModel]] = [] + for error in errors or []: + class ErrorModel(pydantic.BaseModel): + Config = pydantic.config.get_config( dict( self._config_args, title=error.__name__, json_schema_extra=dict(description=f'**{error.CODE}** {error.MESSAGE}'), ), - ), - ) for error in errors or [] - ) + ) + __root__: JsonRpcResponseError[JsonRpcError[Literal[error.CODE], Any]] # type: ignore[name-defined] + + error_models.append(pd.create_model(error.__name__, __base__=ErrorModel)) class ResponseModel(pydantic.BaseModel): Config = pydantic.config.get_config(dict(title=f"{to_camel(method_name)}Response")) diff --git a/pjrpc/server/validators/pydantic_v1.py b/pjrpc/server/validators/pydantic_v1.py index 848d409..3ae53cb 100644 --- a/pjrpc/server/validators/pydantic_v1.py +++ b/pjrpc/server/validators/pydantic_v1.py @@ -71,7 +71,7 @@ def validate_params(self, params: Optional['JsonRpcParamsT']) -> dict[str, Any]: except pydantic.ValidationError as e: raise base.ValidationError(*e.errors()) from e - return {field_name: obj.__dict__[field_name] for field_name in fields} + return {field_name: obj.__dict__[field_name] for field_name in model_fields} def _build_validation_model(self, method_name: str) -> type[pydantic.BaseModel]: schema = self._build_validation_schema(self._signature) diff --git a/pyproject.toml b/pyproject.toml index 841feb3..971c699 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "pjrpc" -version = "2.0.0" +version = "2.0.1" description = "Extensible JSON-RPC library" authors = ["Dmitry Pershin "] license = "Unlicense"