diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index dc9f4c2..d58bdbd 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -1,6 +1,12 @@
Changelog
=========
+1.11.0 (2024-12-08)
+------------------
+
+- added exclude_param argument to be able to exclude a json-rpc parameter from validation and schema extraction.
+
+
1.10.1 (2024-11-13)
------------------
diff --git a/examples/aiohttp_dependency_injection.py b/examples/aiohttp_dependency_injection.py
new file mode 100644
index 0000000..35b7d50
--- /dev/null
+++ b/examples/aiohttp_dependency_injection.py
@@ -0,0 +1,81 @@
+import uuid
+from typing import Any, Optional, Type
+
+import pydantic
+from aiohttp import web
+from dependency_injector import containers, providers
+from dependency_injector.wiring import Provide, inject
+
+import pjrpc.server
+import pjrpc.server.specs.extractors.pydantic
+from pjrpc.server.integration import aiohttp
+from pjrpc.server.specs import extractors
+from pjrpc.server.specs import openapi as specs
+from pjrpc.server.validators import pydantic as validators
+
+
+def is_di_injected(name: str, annotation: Type[Any], default: Any) -> bool:
+ return type(default) is Provide
+
+
+methods = pjrpc.server.MethodRegistry()
+validator = validators.PydanticValidator(exclude_param=is_di_injected)
+
+
+class UserService:
+ def __init__(self):
+ self._users = {}
+
+ def add_user(self, user: dict) -> str:
+ user_id = uuid.uuid4().hex
+ self._users[user_id] = user
+
+ return user_id
+
+ def get_user(self, user_id: uuid.UUID) -> Optional[dict]:
+ return self._users.get(user_id)
+
+
+class Container(containers.DeclarativeContainer):
+ wiring_config = containers.WiringConfiguration(modules=["__main__"])
+ user_service = providers.Factory(UserService)
+
+
+class User(pydantic.BaseModel):
+ name: str
+ surname: str
+ age: int
+
+
+@specs.annotate(summary='Creates a user', tags=['users'])
+@methods.add(context='request')
+@validator.validate
+@inject
+async def add_user(
+ request: web.Request,
+ user: User,
+ user_service: UserService = Provide[Container.user_service],
+) -> dict:
+ user_dict = user.model_dump()
+ user_id = user_service.add_user(user_dict)
+
+ return {'id': user_id, **user_dict}
+
+
+jsonrpc_app = aiohttp.Application(
+ '/api/v1',
+ specs=[
+ specs.OpenAPI(
+ info=specs.Info(version="1.0.0", title="User storage"),
+ schema_extractor=extractors.pydantic.PydanticSchemaExtractor(exclude_param=is_di_injected),
+ ui=specs.SwaggerUI(),
+ ),
+ ],
+)
+jsonrpc_app.dispatcher.add_methods(methods)
+jsonrpc_app.app['users'] = {}
+
+jsonrpc_app.app.container = Container()
+
+if __name__ == "__main__":
+ web.run_app(jsonrpc_app.app, host='localhost', port=8080)
diff --git a/pjrpc/__about__.py b/pjrpc/__about__.py
index eda4339..021126c 100644
--- a/pjrpc/__about__.py
+++ b/pjrpc/__about__.py
@@ -2,7 +2,7 @@
__description__ = 'Extensible JSON-RPC library'
__url__ = 'https://github.com/dapper91/pjrpc'
-__version__ = '1.10.1'
+__version__ = '1.11.0'
__author__ = 'Dmitry Pershin'
__email__ = 'dapper1291@gmail.com'
diff --git a/pjrpc/server/specs/extractors/docstring.py b/pjrpc/server/specs/extractors/docstring.py
index 2439348..e5ca377 100644
--- a/pjrpc/server/specs/extractors/docstring.py
+++ b/pjrpc/server/specs/extractors/docstring.py
@@ -6,13 +6,20 @@
from pjrpc.common.typedefs import MethodType
from pjrpc.server.specs.extractors import BaseSchemaExtractor, JsonRpcError
from pjrpc.server.specs.schemas import build_request_schema, build_response_schema
+from pjrpc.server.typedefs import ExcludeFunc
class DocstringSchemaExtractor(BaseSchemaExtractor):
"""
docstring method specification generator.
+
+ :param exclude_param: a function that decides if the parameters must be excluded
+ from schema (useful for dependency injection)
"""
+ def __init__(self, exclude_param: Optional[ExcludeFunc] = None):
+ self._exclude_param = exclude_param or (lambda *args: False)
+
def extract_params_schema(
self,
method_name: str,
@@ -26,7 +33,7 @@ def extract_params_schema(
if method.__doc__:
doc = docstring_parser.parse(method.__doc__)
for param in doc.params:
- if param.arg_name in exclude:
+ if param.arg_name in exclude or self._exclude_param(param.arg_name, None, None):
continue
parameters_schema[param.arg_name] = {
diff --git a/pjrpc/server/specs/extractors/pydantic.py b/pjrpc/server/specs/extractors/pydantic.py
index 9d0e289..c2454e8 100644
--- a/pjrpc/server/specs/extractors/pydantic.py
+++ b/pjrpc/server/specs/extractors/pydantic.py
@@ -6,6 +6,7 @@
from pjrpc.common import exceptions
from pjrpc.common.typedefs import MethodType
from pjrpc.server.specs.extractors import BaseSchemaExtractor
+from pjrpc.server.typedefs import ExcludeFunc
def to_camel(string: str) -> str:
@@ -86,9 +87,12 @@ class PydanticSchemaExtractor(BaseSchemaExtractor):
Pydantic method specification extractor.
:param config_args: model configuration parameters
+ :param exclude_param: a function that decides if the parameters must be excluded
+ from schema (useful for dependency injection)
"""
- def __init__(self, **config_args: Any):
+ def __init__(self, exclude_param: Optional[ExcludeFunc] = None, **config_args: Any):
+ self._exclude_param = exclude_param or (lambda *args: False)
self._config_args = config_args
def extract_params_schema(
@@ -223,7 +227,7 @@ def _build_params_model(
field_definitions: Dict[str, Any] = {}
for param in signature.parameters.values():
- if param.name in exclude:
+ if param.name in exclude or self._exclude_param(param.name, param.annotation, param.default):
continue
if param.kind in [inspect.Parameter.POSITIONAL_OR_KEYWORD, inspect.Parameter.KEYWORD_ONLY]:
diff --git a/pjrpc/server/typedefs.py b/pjrpc/server/typedefs.py
index 75db74c..17f358b 100644
--- a/pjrpc/server/typedefs.py
+++ b/pjrpc/server/typedefs.py
@@ -1,6 +1,6 @@
from __future__ import annotations
-from typing import Any, Awaitable, Callable, Optional, Union
+from typing import Any, Awaitable, Callable, Optional, Type, Union
import pjrpc.common.exceptions
from pjrpc.common import Request, Response, UnsetType
@@ -12,6 +12,7 @@
'MiddlewareResponse',
'MiddlewareType',
'ErrorHandlerType',
+ 'ExcludeFunc',
'ResponseOrUnset',
'ContextType',
]
@@ -56,3 +57,7 @@
pjrpc.exceptions.JsonRpcError,
]
'''Synchronous server error handler''' # for sphinx autodoc
+
+
+ExcludeFunc = Callable[[str, Optional[Type[Any]], Optional[Any]], bool]
+'''Parameter exclude function''' # for sphinx autodoc
diff --git a/pjrpc/server/validators/__init__.py b/pjrpc/server/validators/__init__.py
index 957a66f..3c7cf44 100644
--- a/pjrpc/server/validators/__init__.py
+++ b/pjrpc/server/validators/__init__.py
@@ -2,9 +2,10 @@
JSON-RPC method parameters validators.
"""
-from .base import BaseValidator, ValidationError
+from .base import BaseValidator, ExcludeFunc, ValidationError
__all__ = [
'BaseValidator',
+ 'ExcludeFunc',
'ValidationError',
]
diff --git a/pjrpc/server/validators/base.py b/pjrpc/server/validators/base.py
index 2d5920a..9cad33a 100644
--- a/pjrpc/server/validators/base.py
+++ b/pjrpc/server/validators/base.py
@@ -1,9 +1,10 @@
import functools as ft
import inspect
-from typing import Any, Dict, Iterable, Optional, Tuple
+from typing import Any, Dict, Iterable, List, Optional, Tuple
from pjrpc.common.typedefs import JsonRpcParams, MethodType
from pjrpc.server import utils
+from pjrpc.server.typedefs import ExcludeFunc
class ValidationError(Exception):
@@ -15,8 +16,14 @@ class ValidationError(Exception):
class BaseValidator:
"""
Base method parameters validator. Uses :py:func:`inspect.signature` for validation.
+
+ :param exclude_param: a function that decides if the parameters must be excluded
+ from validation (useful for dependency injection)
"""
+ def __init__(self, exclude_param: Optional[ExcludeFunc] = None):
+ self._exclude_param = exclude_param or (lambda *args: False)
+
def validate(self, maybe_method: Optional[MethodType] = None, **kwargs: Any) -> MethodType:
"""
Decorator marks a method the parameters of which to be validated when calling it using JSON-RPC protocol.
@@ -73,7 +80,7 @@ def bind(self, signature: inspect.Signature, params: Optional['JsonRpcParams'])
raise ValidationError(str(e)) from e
@ft.lru_cache(None)
- def signature(self, method: MethodType, exclude: Tuple[str]) -> inspect.Signature:
+ def signature(self, method: MethodType, exclude: Tuple[str, ...]) -> inspect.Signature:
"""
Returns method signature.
@@ -83,8 +90,10 @@ def signature(self, method: MethodType, exclude: Tuple[str]) -> inspect.Signatur
"""
signature = inspect.signature(method)
- parameters = signature.parameters.copy()
- for item in exclude:
- parameters.pop(item, None)
- return signature.replace(parameters=tuple(parameters.values()))
+ method_parameters: List[inspect.Parameter] = []
+ for param in signature.parameters.values():
+ if param.name not in exclude and not self._exclude_param(param.name, param.annotation, param.default):
+ method_parameters.append(param)
+
+ return signature.replace(parameters=method_parameters)
diff --git a/pjrpc/server/validators/jsonschema.py b/pjrpc/server/validators/jsonschema.py
index 75b0f7f..68558d7 100644
--- a/pjrpc/server/validators/jsonschema.py
+++ b/pjrpc/server/validators/jsonschema.py
@@ -3,6 +3,7 @@
import jsonschema
from pjrpc.common.typedefs import JsonRpcParams, MethodType
+from pjrpc.server.typedefs import ExcludeFunc
from . import base
@@ -12,9 +13,12 @@ class JsonSchemaValidator(base.BaseValidator):
Parameters validator based on `jsonschema `_ library.
:param kwargs: default jsonschema validator arguments
+ :param exclude_param: a function that decides if the parameters must be excluded
+ from validation (useful for dependency injection)
"""
- def __init__(self, **kwargs: Any):
+ def __init__(self, exclude_param: Optional[ExcludeFunc] = None, **kwargs: Any):
+ super().__init__(exclude_param=exclude_param)
kwargs.setdefault('types', {'array': (list, tuple)})
self.default_kwargs = kwargs
diff --git a/pjrpc/server/validators/pydantic.py b/pjrpc/server/validators/pydantic.py
index 8aa1a42..686f58e 100644
--- a/pjrpc/server/validators/pydantic.py
+++ b/pjrpc/server/validators/pydantic.py
@@ -5,6 +5,7 @@
import pydantic
from pjrpc.common.typedefs import JsonRpcParams
+from pjrpc.server.typedefs import ExcludeFunc
from . import base
@@ -16,9 +17,12 @@ class PydanticValidator(base.BaseValidator):
:param coerce: if ``True`` returns converted (coerced) parameters according to parameter type annotation
otherwise returns parameters as is
+ :param exclude_param: a function that decides if the parameters must be excluded
+ from validation (useful for dependency injection)
"""
- def __init__(self, coerce: bool = True, **config_args: Any):
+ def __init__(self, coerce: bool = True, exclude_param: Optional[ExcludeFunc] = None, **config_args: Any):
+ super().__init__(exclude_param=exclude_param)
self._coerce = coerce
config_args.setdefault('extra', 'forbid')
diff --git a/pyproject.toml b/pyproject.toml
index 8a38945..42b00b5 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "pjrpc"
-version = "1.10.1"
+version = "1.11.0"
description = "Extensible JSON-RPC library"
authors = ["Dmitry Pershin "]
license = "Unlicense"