Skip to content

Commit

Permalink
Merge pull request #35 from dapper91/dev
Browse files Browse the repository at this point in the history
- pytest integration bug fixed
- ViewMethod copy bug fixed
- pydantic required version increased
- openapi/openrpc specification definitions support implemented
  • Loading branch information
dapper91 committed Aug 24, 2021
2 parents 464018d + b478523 commit 4c744e1
Show file tree
Hide file tree
Showing 21 changed files with 136 additions and 64 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
Changelog
=========

1.3.1 (2021-08-24)
------------------

- pytest integration bug fixed
- ViewMethod copy bug fixed
- pydantic required version increased
- openapi/openrpc specification definitions support implemented


1.3.0 (2021-08-13)
------------------

Expand Down
8 changes: 6 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
pjrpc
=====

.. image:: https://static.pepy.tech/personalized-badge/pjrpc?period=month&units=international_system&left_color=grey&right_color=orange&left_text=Downloads/month
:target: https://pepy.tech/project/pjrpc
:alt: Downloads/month
.. image:: https://travis-ci.org/dapper91/pjrpc.svg?branch=master
:target: https://travis-ci.org/dapper91/pjrpc
:alt: Build status
Expand All @@ -24,6 +27,7 @@ that can be easily extended and integrated in your project without writing a lot

Features:

- framework agnostic
- intuitive api
- extendability
- synchronous and asynchronous client backed
Expand Down Expand Up @@ -55,7 +59,7 @@ Extra requirements
- `pydantic <https://pydantic-docs.helpmanual.io/>`_
- `requests <https://requests.readthedocs.io>`_
- `httpx <https://www.python-httpx.org/>`_
- `openapi-ui-bundles <https://github.com/dapper91/openapi_ui_bundles>`_
- `openapi-ui-bundles <https://github.com/dapper91/python-openapi-ui-bundles>`_


Documentation
Expand Down Expand Up @@ -316,7 +320,7 @@ necessary). ``pjrpc`` will be validating method parameters and returning informa
return {'id': user_id, **user.dict()}
class JSONEncoder(pjrpc.common.JSONEncoder):
class JSONEncoder(pjrpc.server.JSONEncoder):
def default(self, o):
if isinstance(o, uuid.UUID):
Expand Down
4 changes: 4 additions & 0 deletions docs/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ Welcome to pjrpc's documentation!
=================================


.. image:: https://static.pepy.tech/personalized-badge/pjrpc?period=month&units=international_system&left_color=grey&right_color=orange&left_text=Downloads/month
:target: https://pepy.tech/project/pjrpc
:alt: Downloads/month
.. image:: https://travis-ci.org/dapper91/pjrpc.svg?branch=master
:target: https://travis-ci.org/dapper91/pjrpc
:alt: Build status
Expand All @@ -29,6 +32,7 @@ that can be easily extended and integrated in your project without writing a lot

Features:

- :doc:`framework/library agnostic <pjrpc/examples>`
- :doc:`intuitive interface <pjrpc/quickstart>`
- :doc:`extensibility <pjrpc/extending>`
- :doc:`synchronous and asynchronous client backends <pjrpc/client>`
Expand Down
10 changes: 5 additions & 5 deletions docs/source/pjrpc/quickstart.rst
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ necessary). ``pjrpc`` will be validating method parameters and returning informa
return {'id': user_id, **user.dict()}
class JSONEncoder(pjrpc.common.JSONEncoder):
class JSONEncoder(pjrpc.server.JSONEncoder):
def default(self, o):
if isinstance(o, uuid.UUID):
Expand Down Expand Up @@ -362,7 +362,7 @@ On the server side everything is also pretty straightforward:
app.run(port=80)
Open API specification
OpenAPI specification
______________________

``pjrpc`` has built-in `OpenAPI <https://swagger.io/specification/>`_ and `OpenRPC <https://spec.open-rpc.org/#introduction>`_
Expand Down Expand Up @@ -620,18 +620,18 @@ Swagger UI:

.. image:: ../_static/swagger-ui-screenshot.png
:width: 1024
:alt: Open API full example
:alt: OpenAPI full example

RapiDoc:
~~~~~~~~

.. image:: ../_static/rapidoc-screenshot.png
:width: 1024
:alt: Open API cli example
:alt: OpenAPI cli example

ReDoc:
~~~~~~

.. image:: ../_static/redoc-screenshot.png
:width: 1024
:alt: Open API method example
:alt: OpenAPI method example
4 changes: 2 additions & 2 deletions docs/source/pjrpc/testing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ Testing aiohttp server code is very straightforward:
import pjrpc.server
from pjrpc.server.integration import aiohttp
from pjrpc.client.backend import aiohttp as aiohttp_client
from pjrpc.client.backend import aiohttp as pjrpc_aiohttp_client
methods = pjrpc.server.MethodRegistry()
Expand All @@ -86,7 +86,7 @@ Testing aiohttp server code is very straightforward:
async def test_sum(aiohttp_client, loop):
session = await aiohttp_client(jsonrpc_app.app)
client = aiohttp_client.Client('http://localhost/api/v1', session=session)
client = pjrpc_aiohttp_client.Client('http://localhost/api/v1', session=session)
result = await client.sum(a=1, b=1)
assert result == 2
Expand Down
2 changes: 1 addition & 1 deletion docs/source/pjrpc/validation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ that's it. ``pjrpc`` will be validating method parameters and returning informat
return {'id': user_id, **user.dict()}
class JSONEncoder(pjrpc.common.JSONEncoder):
class JSONEncoder(pjrpc.server.JSONEncoder):
def default(self, o):
if isinstance(o, uuid.UUID):
Expand Down
6 changes: 3 additions & 3 deletions docs/source/pjrpc/webui.rst
Original file line number Diff line number Diff line change
Expand Up @@ -290,18 +290,18 @@ Swagger UI:

.. image:: ../_static/swagger-ui-screenshot.png
:width: 1024
:alt: Open API full example
:alt: OpenAPI full example

RapiDoc:
~~~~~~~~

.. image:: ../_static/rapidoc-screenshot.png
:width: 1024
:alt: Open API cli example
:alt: OpenAPI cli example

ReDoc:
~~~~~~

.. image:: ../_static/redoc-screenshot.png
:width: 1024
:alt: Open API method example
:alt: OpenAPI method example
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.3.0'
__version__ = '1.3.1'

__author__ = 'Dmitry Pershin'
__email__ = '[email protected]'
Expand Down
4 changes: 2 additions & 2 deletions pjrpc/client/integrations/pytest.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,12 +186,12 @@ def _cleanup_matches(self, endpoint: str, version: str = '2.0', method_name: Opt
if not self._matches[endpoint]:
self._matches.pop(endpoint)

def _on_request(self, origin_self: Any, request_text: str) -> str:
def _on_request(self, origin_self: Any, request_text: str, is_notification: bool = False, **kwargs: Any) -> str:
endpoint = origin_self._endpoint
matches = self._matches.get(endpoint)
if matches is None:
if self._passthrough:
return self._patcher.temp_original(origin_self, request_text)
return self._patcher.temp_original(origin_self, request_text, is_notification, **kwargs)
else:
raise ConnectionRefusedError()

Expand Down
2 changes: 1 addition & 1 deletion pjrpc/common/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ def from_json(cls, json_data: Json) -> 'JsonRpcError':
:param json_data: json data the error to be deserialized from
:returns: deserialized error
:raises: :py:class:`pjrpc.common.exception.DeserializationError` if format is incorrect
:raises: :py:class:`pjrpc.common.exceptions.DeserializationError` if format is incorrect
"""

try:
Expand Down
8 changes: 4 additions & 4 deletions pjrpc/common/v20.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ def from_json(cls, json_data: Json, error_cls: Type[JsonRpcError] = JsonRpcError
:param json_data: data the response to be deserialized from
:param error_cls: error class
:returns: response object
:raises: :py:class:`pjrpc.common.exception.DeserializationError` if format is incorrect
:raises: :py:class:`pjrpc.common.exceptions.DeserializationError` if format is incorrect
"""

try:
Expand Down Expand Up @@ -81,7 +81,7 @@ def result(self) -> Any:
@property
def error(self) -> Union[UnsetType, JsonRpcError]:
"""
Response error. If the response has succeeded returns :py:data:`pjrpc.common.UNSET`.
Response error. If the response has succeeded returns :py:data:`pjrpc.common.common.UNSET`.
"""

return self._error
Expand Down Expand Up @@ -187,7 +187,7 @@ def from_json(cls, json_data: Json) -> 'Request':
:param json_data: data the request to be deserialized from
:returns: request object
:raises: :py:class:`pjrpc.common.exception.DeserializationError` if format is incorrect
:raises: :py:class:`pjrpc.common.exceptions.DeserializationError` if format is incorrect
"""

try:
Expand Down Expand Up @@ -332,7 +332,7 @@ def from_json(cls, json_data: Json, error_cls: Type[JsonRpcError] = JsonRpcError
@property
def error(self) -> Union[UnsetType, JsonRpcError]:
"""
Response error. If the response has succeeded returns :py:data:`pjrpc.common.UNSET`.
Response error. If the response has succeeded returns :py:data:`pjrpc.common.common.UNSET`.
"""

return self._error
Expand Down
7 changes: 5 additions & 2 deletions pjrpc/server/dispatcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,11 @@ def bind(self, params: Optional[Union[list, dict]], context: Optional[Any] = Non

return ft.partial(method, **method_params)

def copy(self, **kwargs) -> 'Method':
return super().copy(view_cls=self.view_cls, method_name=self.method_name, **kwargs)
def copy(self, **kwargs) -> 'ViewMethod':
cls_kwargs = dict(name=self.name, context=self.context)
cls_kwargs.update(kwargs)

return ViewMethod(view_cls=self.view_cls, method_name=self.method_name, **cls_kwargs)


class ViewMixin:
Expand Down
1 change: 1 addition & 0 deletions pjrpc/server/specs/extractors/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class Schema:
summary: str = UNSET
description: str = UNSET
deprecated: bool = UNSET
definitions: Dict[str, Any] = UNSET


@dc.dataclass(frozen=True)
Expand Down
12 changes: 8 additions & 4 deletions pjrpc/server/specs/extractors/pydantic.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ class PydanticSchemaExtractor(BaseSchemaExtractor):
Pydantic method specification extractor.
"""

def __init__(self, ref_template: str = '#/components/schemas/{model}'):
self._ref_template = ref_template

def extract_params_schema(self, method: Callable, exclude: Iterable[str] = ()) -> Dict[str, Schema]:
exclude = set(exclude)
signature = inspect.signature(method)
Expand All @@ -28,11 +31,10 @@ def extract_params_schema(self, method: Callable, exclude: Iterable[str] = ()) -
)

params_model = pd.create_model('RequestModel', **field_definitions)
model_schema = params_model.schema(ref_template='{model}')
model_schema = params_model.schema(ref_template=self._ref_template)

parameters_schema = {}
for param_name, param_schema in model_schema['properties'].items():
param_schema = self._extract_field_schema(model_schema, field_name=param_name)
required = param_name in model_schema.get('required', [])

parameters_schema[param_name] = Schema(
Expand All @@ -41,6 +43,7 @@ def extract_params_schema(self, method: Callable, exclude: Iterable[str] = ()) -
description=param_schema.get('description', UNSET),
deprecated=param_schema.get('deprecated', UNSET),
required=required,
definitions=model_schema.get('definitions'),
)

return parameters_schema
Expand All @@ -56,9 +59,9 @@ def extract_result_schema(self, method: Callable) -> Schema:
return_annotation = result.return_annotation

result_model = pd.create_model('ResultModel', result=(return_annotation, pd.fields.Undefined))
model_schema = result_model.schema(ref_template='{model}')
model_schema = result_model.schema(ref_template=self._ref_template)

result_schema = self._extract_field_schema(model_schema, field_name='result')
result_schema = model_schema['properties']['result']
required = 'result' in model_schema.get('required', [])
if not required:
result_schema['nullable'] = 'true'
Expand All @@ -69,6 +72,7 @@ def extract_result_schema(self, method: Callable) -> Schema:
description=result_schema.get('description', UNSET),
deprecated=result_schema.get('deprecated', UNSET),
required=required,
definitions=model_schema.get('definitions'),
)

return result_schema
Expand Down
13 changes: 11 additions & 2 deletions pjrpc/server/specs/openapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -289,15 +289,17 @@ def __post_init__(self):
field.name = 'in'


@dc.dataclass(frozen=True)
@dc.dataclass(frozen=False)
class Components:
"""
Holds a set of reusable objects for different aspects of the OAS.
:param securitySchemes: an object to hold reusable Security Scheme Objects
:param schemas: the definition of input and output data types
"""

securitySchemes: Dict[str, SecurityScheme] = UNSET
schemas: Dict[str, Dict[str, Any]] = dc.field(default_factory=dict)


@dc.dataclass(frozen=True)
Expand Down Expand Up @@ -571,12 +573,19 @@ def schema(self, path: str, methods: Iterable[Method]) -> dict:

for param_name, param_schema in method_spec['params_schema'].items():
request_schema['properties']['params']['properties'][param_name] = param_schema.schema

if param_schema.required:
required_params = request_schema['properties']['params'].setdefault('required', [])
required_params.append(param_name)

if param_schema.definitions:
self.components.schemas.update(param_schema.definitions)

response_schema = copy.deepcopy(RESPONSE_SCHEMA)
response_schema['oneOf'][0]['properties']['result'] = method_spec['result_schema'].schema
result_schema = method_spec['result_schema']
response_schema['oneOf'][0]['properties']['result'] = result_schema.schema
if result_schema.definitions:
self.components.schemas.update(result_schema.definitions)

self.paths[f'{path}#{method.name}'] = Path(
post=Operation(
Expand Down
20 changes: 20 additions & 0 deletions pjrpc/server/specs/openrpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,17 @@ class MethodInfo:
servers: List[Server] = UNSET


@dc.dataclass(frozen=True)
class Components:
"""
Set of reusable objects for different aspects of the OpenRPC.
:param schemas: reusable Schema Objects
"""

schemas: Dict[str, Any] = dc.field(default_factory=dict)


def annotate(
params_schema: List[ContentDescriptor] = UNSET,
result_schema: ContentDescriptor = UNSET,
Expand Down Expand Up @@ -300,6 +311,7 @@ class OpenRPC(Specification):
servers: List[Server] = UNSET
externalDocs: ExternalDocumentation = UNSET
openrpc: str = '1.0.0'
components: Components = UNSET

def __init__(
self,
Expand All @@ -317,6 +329,7 @@ def __init__(
self.externalDocs = external_docs
self.openrpc = openrpc
self.methods = []
self.components = Components()

self._schema_extractor = schema_extractor

Expand Down Expand Up @@ -391,6 +404,13 @@ def schema(self, path: str, methods: Iterable[Method]) -> dict:
),
)

for param_schema in params_schema.values():
if param_schema.definitions:
self.components.schemas.update(param_schema.definitions)

if result_schema.definitions:
self.components.schemas.update(result_schema.definitions)

return dc.asdict(
self,
dict_factory=lambda iterable: dict(
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ def run_tests(self):
'flask': ['flask~=1.0'],
'jsonschema': ['jsonschema~=3.0'],
'kombu': ['kombu~=5.0'],
'pydantic': ['pydantic~=1.0'],
'pydantic': ['pydantic~=1.8.0'],
'requests': ['requests~=2.0'],
'httpx': ['requests~=0.0'],
'docstring-parser': ['docstring-parser~=0.0'],
Expand Down
Loading

0 comments on commit 4c744e1

Please sign in to comment.