Skip to content

Commit

Permalink
Merge pull request #112 from dapper91/dev
Browse files Browse the repository at this point in the history
- added exclude_param argument to be able to exclude a json-rpc parameter from validation and schema extraction.
  • Loading branch information
dapper91 authored Dec 8, 2024
2 parents d75e055 + ee5a175 commit 4bee6ba
Show file tree
Hide file tree
Showing 11 changed files with 136 additions and 15 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -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)
------------------

Expand Down
81 changes: 81 additions & 0 deletions examples/aiohttp_dependency_injection.py
Original file line number Diff line number Diff line change
@@ -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)
2 changes: 1 addition & 1 deletion pjrpc/__about__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__ = '[email protected]'
Expand Down
9 changes: 8 additions & 1 deletion pjrpc/server/specs/extractors/docstring.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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] = {
Expand Down
8 changes: 6 additions & 2 deletions pjrpc/server/specs/extractors/pydantic.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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]:
Expand Down
7 changes: 6 additions & 1 deletion pjrpc/server/typedefs.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -12,6 +12,7 @@
'MiddlewareResponse',
'MiddlewareType',
'ErrorHandlerType',
'ExcludeFunc',
'ResponseOrUnset',
'ContextType',
]
Expand Down Expand Up @@ -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
3 changes: 2 additions & 1 deletion pjrpc/server/validators/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@
JSON-RPC method parameters validators.
"""

from .base import BaseValidator, ValidationError
from .base import BaseValidator, ExcludeFunc, ValidationError

__all__ = [
'BaseValidator',
'ExcludeFunc',
'ValidationError',
]
21 changes: 15 additions & 6 deletions pjrpc/server/validators/base.py
Original file line number Diff line number Diff line change
@@ -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):
Expand All @@ -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.
Expand Down Expand Up @@ -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.
Expand All @@ -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)
6 changes: 5 additions & 1 deletion pjrpc/server/validators/jsonschema.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import jsonschema

from pjrpc.common.typedefs import JsonRpcParams, MethodType
from pjrpc.server.typedefs import ExcludeFunc

from . import base

Expand All @@ -12,9 +13,12 @@ class JsonSchemaValidator(base.BaseValidator):
Parameters validator based on `jsonschema <https://python-jsonschema.readthedocs.io/en/stable/>`_ 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

Expand Down
6 changes: 5 additions & 1 deletion pjrpc/server/validators/pydantic.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import pydantic

from pjrpc.common.typedefs import JsonRpcParams
from pjrpc.server.typedefs import ExcludeFunc

from . import base

Expand All @@ -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')
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "pjrpc"
version = "1.10.1"
version = "1.11.0"
description = "Extensible JSON-RPC library"
authors = ["Dmitry Pershin <[email protected]>"]
license = "Unlicense"
Expand Down

0 comments on commit 4bee6ba

Please sign in to comment.