1
0

Initial public releases

* `bthlabs-jsonrpc-aiohttp` v1.0.0
* `bthlabs-jsonrpc-core` v1.0.0
* `bthlabs-jsonrpc-django` v1.0.0
This commit is contained in:
2022-06-04 10:41:53 +02:00
commit c75ea4ea9d
111 changed files with 7193 additions and 0 deletions

View File

@@ -0,0 +1,19 @@
Copyright (c) 2022-present Tomek Wójcik <contact@bthlabs.pl>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -0,0 +1,57 @@
bthlabs-jsonrpc-core
====================
Extensible framework for Python JSONRPC implementations.
`Docs`_ | `Source repository`_
Overview
--------
BTHLabs JSONRPC is a set of Python libraries that provide extensible framework
for adding JSONRPC interfaces to existing Python Web applications.
The *core* package acts as a foundation for framework-specific integrations.
Integrations
------------
BTHLabs JSONRPC provides integration packages for specific Web frameworks.
**Django**
Django integration is provided by ``bthlabs-jsonrpc-django`` package.
+-------------------+-----------------------------------------------------+
| PyPI | https://pypi.org/project/bthlabs-jsonrpc-django/ |
+-------------------+-----------------------------------------------------+
| Docs | https://projects.bthlabs.pl/bthlabs-jsonrpc/django/ |
+-------------------+-----------------------------------------------------+
| Source repository | https://git.bthlabs.pl/tomekwojcik/bthlabs-jsonrpc/ |
+-------------------+-----------------------------------------------------+
**aiohttp**
aiohttp integration is provided by ``bthlabs-jsonrpc-aiohttp`` package.
+-------------------+------------------------------------------------------+
| PyPI | https://pypi.org/project/bthlabs-jsonrpc-aiohttp/ |
+-------------------+------------------------------------------------------+
| Docs | https://projects.bthlabs.pl/bthlabs-jsonrpc/aiohttp/ |
+-------------------+------------------------------------------------------+
| Source repository | https://git.bthlabs.pl/tomekwojcik/bthlabs-jsonrpc/ |
+-------------------+------------------------------------------------------+
Author
------
*bthlabs-jsonrpc-core* is developed by `Tomek Wójcik`_.
License
-------
*bthlabs-jsonrpc-core* is licensed under the MIT License.
.. _Docs: https://projects.bthlabs.pl/bthlabs-jsonrpc/core/
.. _Source repository: https://git.bthlabs.pl/tomekwojcik/bthlabs-jsonrpc/
.. _Tomek Wójcik: https://www.bthlabs.pl/

View File

@@ -0,0 +1,14 @@
# -*- coding: utf-8 -*-
# django-jsonrpc-core | (c) 2022-present Tomek Wójcik | MIT License
from .decorators import register_method # noqa
from .exceptions import ( # noqa
BaseJSONRPCError,
JSONRPCAccessDeniedError,
JSONRPCInternalError,
JSONRPCParseError,
JSONRPCSerializerError,
)
from .executor import Executor # noqa
from .serializer import JSONRPCSerializer # noqa
__version__ = '1.0.0'

View File

@@ -0,0 +1,35 @@
# -*- coding: utf-8 -*-
# django-jsonrpc-core | (c) 2022-present Tomek Wójcik | MIT License
import typing
from bthlabs_jsonrpc_core.registry import MethodRegistry
def register_method(method: str,
namespace: typing.Optional[str] = None,
) -> typing.Callable:
"""
Registers the decorated function as JSONRPC *method* in *namespace*.
If *namespace* is omitted, the function will be registered in the default
namespace.
Example:
.. code-block:: python
@register_method('example')
def example(a, b):
return a + b
"""
if namespace is None:
namespace = MethodRegistry.DEFAULT_NAMESPACE
def decorator(handler: typing.Callable) -> typing.Callable:
registry = MethodRegistry.shared_registry()
registry.register_method(namespace, method, handler)
handler.jsonrpc_method = method
handler.jsonrpc_namespace = namespace
return handler
return decorator

View File

@@ -0,0 +1,99 @@
# -*- coding: utf-8
# django-jsonrpc-core | (c) 2022-present Tomek Wójcik | MIT License
class BaseJSONRPCError(Exception):
"""
Base class for JSONRPC exceptions.
If *data* is provided, it'll be added to the exception's response payload.
"""
#: Error code
ERROR_CODE: int = -32001
#: Error message
ERROR_MESSAGE: str = 'JSONRPC Error'
def __init__(self, data=None):
self.data = data
def to_rpc(self) -> dict:
"""Returns payload for :py:class:`JSONRPCSerializer`."""
result = {
'code': self.ERROR_CODE,
'message': self.ERROR_MESSAGE,
}
if self.data:
result['data'] = self.data
return result
class JSONRPCParseError(BaseJSONRPCError):
"""Parse error"""
#: Error code
ERROR_CODE = -32700
#: Error message
ERROR_MESSAGE = 'Parse error'
class JSONRPCInvalidRequestError(BaseJSONRPCError):
"""Invalid request error"""
#: Error code
ERROR_CODE = -32600
#: Error message
ERROR_MESSAGE = 'Invalid Request'
class JSONRPCMethodNotFoundError(BaseJSONRPCError):
"""Method not found error"""
#: Error code
ERROR_CODE = -32601
#: Error message
ERROR_MESSAGE = 'Method not found'
class JSONRPCInvalidParamsError(BaseJSONRPCError):
"""Invalid params error"""
#: Error code
ERROR_CODE = -32602
#: Error message
ERROR_MESSAGE = 'Invalid params'
class JSONRPCInternalError(BaseJSONRPCError):
"""Internal error"""
#: Error code
ERROR_CODE = -32603
#: Error message
ERROR_MESSAGE = 'Internal error'
class JSONRPCSerializerError(BaseJSONRPCError):
"""Serializer error"""
#: Error code
ERROR_CODE = -32002
#: Error message
ERROR_MESSAGE = 'JSONRPCSerializer error'
class JSONRPCAccessDeniedError(BaseJSONRPCError):
"""Access denied error"""
#: Error code
ERROR_CODE = -32003
#: Error message
ERROR_MESSAGE = 'Access denied'

View File

@@ -0,0 +1,397 @@
# -*- coding: utf-8 -*-
# django-jsonrpc-core | (c) 2022-present Tomek Wójcik | MIT License
from contextlib import contextmanager
from dataclasses import dataclass
import json
import logging
import typing
from bthlabs_jsonrpc_core.exceptions import (
BaseJSONRPCError,
JSONRPCInternalError,
JSONRPCInvalidParamsError,
JSONRPCInvalidRequestError,
JSONRPCMethodNotFoundError,
JSONRPCParseError,
)
from bthlabs_jsonrpc_core.registry import MethodRegistry
from bthlabs_jsonrpc_core.serializer import JSONRPCSerializer
LOGGER = logging.getLogger('bthlabs_jsonrpc.core.executor')
class Executor:
"""
*Executor* is the main interface for the integrations. It processes the
JSONRPC request, executes the calls and returns the responses.
*namespace* will be used to look up called methods in the registry. If
omitted, it'll fall back to the default namespace.
Example:
.. code-block:: python
def rpc_handler(request):
executor = Executor()
serializer = executor.execute(request.body)
return JSONResponse(serializer.data)
"""
# pragma mark - Private class attributes
# Supported internal methods.
# These methods will be resolved and handled internally.
INTERNAL_METHODS = ('system.list_methods',)
# The method registry class to use for handler lookups.
registry = MethodRegistry
# The serializer registry class to use for response serialization.
serializer = JSONRPCSerializer
@dataclass
class CallContext:
"""
The context of a single call.
:meta private:
"""
#: Method
method: str
#: Handler
handler: typing.Callable
#: Call args
args: list[typing.Any]
#: Call kwargs
kwargs: dict
#: Call result
result: typing.Optional[typing.Any] = None
@classmethod
def invalid_context(cls):
return cls(None, None, None, None)
@property
def is_valid(self) -> bool:
"""Returns ``True`` if the context is valid."""
return all((
self.method is not None,
self.handler is not None,
self.args is not None,
self.kwargs is not None,
))
@dataclass
class ExecuteContext:
"""
The context of an execute call.
:meta private:
"""
#: List of call results.
results: list
#: The serializer instance.
serializer: typing.Optional[JSONRPCSerializer] = None
# pragma mark - Private interface
def __init__(self, namespace=None):
self.namespace = namespace or MethodRegistry.DEFAULT_NAMESPACE
def get_internal_handler(self, method: str) -> typing.Callable:
"""
Returns the internal handler for *method* or raises
``JSONRPCMethodNotFoundError``.
:meta private:
"""
match method:
case 'system.list_methods':
return self.list_methods
case _:
raise JSONRPCMethodNotFoundError()
def get_calls(self, data: typing.Union[dict, list]) -> list:
"""
Returns the list of calls.
If *data* is a list, it's returned verbatim. If it's a dict, it's
wrapped in a list.
Raises ``JSONRPCInvalidRequestError`` if the effective list of calls
is empty:
:meta private:
"""
result = list()
if isinstance(data, list):
result = data
else:
result.append(data)
if len(result) == 0:
raise JSONRPCInvalidRequestError()
return result
def get_call_spec(self,
call: typing.Any,
) -> tuple[str, typing.Callable, list, dict]:
"""
Validates and pre-processes the *call*.
Returns tuple of *method*, *handler*, *args*, *kwargs*.
:meta private:
"""
method = None
handler = None
args = []
kwargs = {}
try:
assert isinstance(call, dict), JSONRPCInvalidRequestError
assert call.get('jsonrpc', None) == '2.0', JSONRPCInvalidRequestError
method = call.get('method', None)
assert method is not None, JSONRPCInvalidRequestError
if method in self.INTERNAL_METHODS:
handler = self.get_internal_handler(method)
else:
handler = self.registry.shared_registry().get_handler(
self.namespace, method,
)
assert handler is not None, JSONRPCMethodNotFoundError
except AssertionError as exception:
klass = exception.args[0]
raise klass()
call_params = call.get('params', None)
if call_params is not None:
if isinstance(call_params, list):
args = call_params
elif isinstance(call_params, dict):
kwargs = call_params
else:
raise JSONRPCInvalidParamsError()
args = self.enrich_args(args)
kwargs = self.enrich_kwargs(kwargs)
return method, handler, args, kwargs
def process_results(self,
results: list,
) -> typing.Optional[typing.Union[list, dict]]:
"""
Post-processes the *results* and returns responses.
If *results* is a single-element list, the result is a single
response object. Otherwise, it's a list of response objects.
If the effective response is empty (e.g. all the calls were
notifications), returns ``None``.
:meta private:
"""
responses = []
for result in results:
call, call_result = result
response: dict[str, typing.Any] = {
'jsonrpc': '2.0',
}
if call is None:
response['id'] = None
response['error'] = call_result
elif call.get('id', None) is not None:
response['id'] = call['id']
if isinstance(call_result, BaseJSONRPCError):
response['error'] = call_result
else:
response['result'] = call_result
else:
continue
responses.append(response)
if len(responses) == 0:
return None
elif len(responses) == 1:
return responses[0]
return responses
@contextmanager
def call_context(self, execute_context: ExecuteContext, call: dict):
"""
The call context manager. Yields ``CallContext``, which can be
invalid invalid if there was en error processing the call.
Handles errors and the call result accordingly.
:meta private:
"""
method = None
error = None
try:
context = self.CallContext.invalid_context()
try:
method, handler, args, kwargs = self.get_call_spec(call)
context = self.CallContext(method, handler, args, kwargs)
except BaseJSONRPCError as exception:
error = exception
yield context
except Exception as exception:
if isinstance(exception, BaseJSONRPCError):
error = exception
else:
LOGGER.error(
f'Error handling RPC method: {method}!',
exc_info=exception,
)
error = JSONRPCInternalError(str(exception))
finally:
if error is not None:
execute_context.results.append((call, error))
else:
execute_context.results.append((call, context.result))
@contextmanager
def execute_context(self):
"""
The execution context. Yields ``ExecuteContext``.
Handles errors and manages the serializer post execution.
:meta private:
"""
try:
context = self.ExecuteContext([])
yield context
except Exception as exc:
if isinstance(exc, BaseJSONRPCError):
context.results = [(None, exc)]
else:
raise
responses = self.process_results(context.results)
if responses is not None:
context.serializer = self.serializer(responses)
# pragma mark - Public interface
def deserialize_data(self, data: bytes) -> typing.Any:
"""
Deserializes *data* and returns the result.
Raises :py:exc:`JSONRPCParseError` if there was an error in the process.
Subclasses should also raise this exception, so it can be resulting
response object conforms to the spec.
"""
try:
return json.loads(data)
except Exception as exception:
LOGGER.error('Error deserializing RPC call!', exc_info=exception)
raise JSONRPCParseError() from exception
def list_methods(self, *args, **kwargs) -> list[str]:
"""
The handler for ``system.list_methods`` internal method.
Returns list of methods this *Executor* can handle.
"""
result = list(self.INTERNAL_METHODS)
result.extend(MethodRegistry.shared_registry().get_methods(
self.namespace,
))
return result
def enrich_args(self, args: list) -> list:
"""
Hook for subclasses to pass additional args to the handler. The default
implementation returns the *args* verbatim.
Example:
.. code-block:: python
class ExampleExecutor(Executor):
def enrich_args(self, args):
return ['spam', *args]
"""
return [*args]
def enrich_kwargs(self, kwargs: dict) -> dict:
"""
Hook for subclasses to pass additional kwaargs to the handler.
The default implementation returns the *kwargs* verbatim.
Example:
.. code-block:: python
class ExampleExecutor(Executor):
def enrich_kwargs(self, kwargs):
return {'spam': True, **kwargs}
"""
return {**kwargs}
def before_call(self, method: str, args: list, kwargs: dict):
"""
Hook for subclasses to perform additional operations before executing
the call.
If this method raises a subclass of
:py:exc:`BaseJSONRPCError`, it'll be used to construct the response
object directly. Any other exception will be wrapped in
:py:exc:`JSONRPCInternalError`.
The default implementation does nothing.
"""
pass
def execute(self,
payload: typing.Any,
) -> typing.Optional[JSONRPCSerializer]:
"""
Executes the JSONRPC request in *payload*.
Returns an instance of :py:class:`JSONRPCSerializer` or ``None`` if
the list of responses is empty.
"""
with self.execute_context() as execute_context:
data = self.deserialize_data(payload)
calls = self.get_calls(data)
for call in calls:
with self.call_context(execute_context, call) as call_context:
if call_context.is_valid is True:
self.before_call(
call_context.method,
call_context.args,
call_context.kwargs,
)
call_context.result = call_context.handler(
*call_context.args, **call_context.kwargs,
)
return execute_context.serializer

View File

@@ -0,0 +1,28 @@
# -*- coding: utf-8 -*-
# django-jsonrpc-core | (c) 2022-present Tomek Wójcik | MIT License
class MethodRegistry:
INSTANCE = None
DEFAULT_NAMESPACE = 'jsonrpc'
def __init__(self, *args, **kwargs):
self.registry = {}
self.registry[self.DEFAULT_NAMESPACE] = {}
@classmethod
def shared_registry(cls, *args, **kwargs):
if cls.INSTANCE is None:
cls.INSTANCE = cls(*args, **kwargs)
return cls.INSTANCE
def register_method(self, namespace, method, handler):
if namespace not in self.registry:
self.registry[namespace] = {}
self.registry[namespace][method] = handler
def get_methods(self, namespace):
return self.registry.get(namespace, {}).keys()
def get_handler(self, namespace, method):
return self.registry.get(namespace, {}).get(method, None)

View File

@@ -0,0 +1,181 @@
# -*- coding: utf-8 -*-
# django-jsonrpc-core | (c) 2022-present Tomek Wójcik | MIT License
import datetime
import decimal
import typing
import uuid
from bthlabs_jsonrpc_core.exceptions import JSONRPCSerializerError
class JSONRPCSerializer:
"""
Serializer for JSONRPC responses.
This class is responsible for making the respones JSON-serializable.
Sequence types are all converted to lists. Dict-like types are all
converted to plain dicts. Simple types (``bool``, ``float``, ``int`` and
``str``) and ``None`` are returned as they are.
Datetime values are converted to strings using the ISO format. ``UUID`` and
``Decimal`` values are explicitly coerced to strings.
For values of other types, the serializer will try to invoke their
``to_rpc()`` method. If that fails, the serializer will raise
:py:exc:`JSONRPCSerializerError`.
Example:
.. code-block:: python
spam = ['eggs', {'spam': False}, Decimal('42.0')]
serializer = JSONRPCSerializer(spam)
print(serializer.data)
Example with ``to_rpc()``:
.. code-block:: python
class Spam:
def to_rpc(self):
return {
'spam': True
}
spam = ['eggs', Spam(), Decimal('42.0')]
serializer = JSONRPCSerializer(spam)
print(serializer.data)
"""
# Datetime types
DATETIME_TYPES = (datetime.date, datetime.datetime, datetime.time)
# Sequence types
SEQUENCE_TYPES = (set,)
# Simple types
SIMPLE_TYPES = (bool, float, int, str)
# Types that can be coerced to string
STRING_COERCIBLE_TYPES = (uuid.UUID, decimal.Decimal)
def __init__(self, data):
self._data = data
def is_simple_value(self, value: typing.Any) -> bool:
"""
Returns ``True`` if *value* is a simple value.
:meta private:
"""
value_type = type(value)
return (
value is None or value_type in self.SIMPLE_TYPES
)
def is_datetime_value(self, value: typing.Any) -> bool:
"""
Returns ``True`` if *value* is a datetime value.
:meta private:
"""
return type(value) in self.DATETIME_TYPES
def is_sequence_value(self, value: typing.Any) -> bool:
"""
Returns ``True`` if *value* is a sequence value.
:meta private:
"""
return any((
isinstance(value, typing.Sequence),
isinstance(value, typing.Generator),
type(value) in self.SEQUENCE_TYPES,
))
def is_dict_value(self, value: typing.Any) -> bool:
"""
Returns ``True`` if *value* is a simple value.
:meta private:
"""
return isinstance(value, typing.Dict)
def is_string_coercible_value(self, value: typing.Any) -> bool:
"""
Returns ``True`` if *value* is a coercible to string.
:meta private:
"""
return type(value) in self.STRING_COERCIBLE_TYPES
def serialize_datetime(self, value: typing.Any) -> typing.Any:
"""
Serializes a datetime value.
:meta private:
"""
return value.isoformat()
def serialize_sequence(self, value: typing.Any) -> typing.Any:
"""
Serializes a sequence value.
:meta private:
"""
return [self.serialize_value(item) for item in value]
def serialize_dict(self, value: typing.Any) -> typing.Any:
"""
Serializes a dict-like value.
:meta private:
"""
return {
key: self.serialize_value(item) for key, item in value.items()
}
def serialize_string_coercible(self, value: typing.Any) -> typing.Any:
"""
Serializes a string-coercible value.
:meta private:
"""
return str(value)
def serialize_value(self, value: typing.Any) -> typing.Any:
"""
Serializes *value* and returns the result.
:meta private:
"""
if isinstance(value, JSONRPCSerializer):
return value.data
elif self.is_simple_value(value):
return value
if self.is_datetime_value(value):
return self.serialize_datetime(value)
elif self.is_sequence_value(value):
return self.serialize_sequence(value)
elif self.is_dict_value(value):
return self.serialize_dict(value)
elif self.is_string_coercible_value(value):
return self.serialize_string_coercible(value)
elif hasattr(value, 'to_rpc'):
return self.serialize_value(value.to_rpc())
else:
raise JSONRPCSerializerError(
'Object of type {type} is not RPC serializable'.format(
type=type(value),
),
)
return value
@property
def data(self) -> typing.Any:
"""The serialized data."""
if not hasattr(self, '_serialized_data'):
self._serialized_data = self.serialize_value(self._data)
return self._serialized_data

View File

@@ -0,0 +1,20 @@
# Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line, and also
# from the environment for the first two.
SPHINXOPTS ?=
SPHINXBUILD ?= sphinx-build
SOURCEDIR = source
BUILDDIR = build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

View File

@@ -0,0 +1,43 @@
API Documentation
=================
.. module:: bthlabs_jsonrpc_core
This section provides the API documentation for BTHLabs JSONRPC - Core.
Decorators
----------
.. autofunction:: register_method
Exceptions
----------
.. autoexception:: BaseJSONRPCError
:members:
.. autoexception:: JSONRPCAccessDeniedError
:members:
.. autoexception:: JSONRPCInternalError
:members:
.. autoexception:: JSONRPCParseError
:members:
.. autoexception:: JSONRPCSerializerError
:members:
Executor
--------
.. autoclass:: Executor
:members:
Serializer
----------
.. autoclass:: JSONRPCSerializer
:members:

View File

@@ -0,0 +1,57 @@
# Configuration file for the Sphinx documentation builder.
#
# This file only contains a selection of the most common options. For a full
# list see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html
# -- Path setup --------------------------------------------------------------
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
import os
import sys
sys.path.insert(0, os.path.abspath('../../'))
# -- Project information -----------------------------------------------------
project = 'BTHLabs JSONRPC - Core'
copyright = '2022-present Tomek Wójcik'
author = 'Tomek Wójcik'
version = '1.0.0'
# The full version, including alpha/beta/rc tags
release = '1.0.0'
# -- General configuration ---------------------------------------------------
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
'sphinx.ext.autodoc',
]
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This pattern also affects html_static_path and html_extra_path.
exclude_patterns = []
# -- Options for HTML output -------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
html_theme = 'sphinx_rtd_theme'
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']

View File

@@ -0,0 +1,18 @@
BTHLabs JSONRPC - Core
======================
BTHLabs JSONRPC is a set of Python libraries that provide extensible framework
for adding JSONRPC interfaces to existing Python Web applications.
The *core* package acts as a foundation for framework-specific integrations.
.. toctree::
:maxdepth: 2
overview
integrations
.. toctree::
:maxdepth: 2
api

View File

@@ -0,0 +1,30 @@
Integrations
============
BTHLabs JSONRPC provides integration packages for specific Web frameworks.
Django
------
Django integration is provided by ``bthlabs-jsonrpc-django`` package.
+-------------------+-----------------------------------------------------+
| PyPI | https://pypi.org/project/bthlabs-jsonrpc-django/ |
+-------------------+-----------------------------------------------------+
| Docs | https://projects.bthlabs.pl/bthlabs-jsonrpc/django/ |
+-------------------+-----------------------------------------------------+
| Source repository | https://git.bthlabs.pl/tomekwojcik/bthlabs-jsonrpc/ |
+-------------------+-----------------------------------------------------+
aiohttp
-------
aiohttp integration is provided by ``bthlabs-jsonrpc-aiohttp`` package.
+-------------------+------------------------------------------------------+
| PyPI | https://pypi.org/project/bthlabs-jsonrpc-aiohttp/ |
+-------------------+------------------------------------------------------+
| Docs | https://projects.bthlabs.pl/bthlabs-jsonrpc/aiohttp/ |
+-------------------+------------------------------------------------------+
| Source repository | https://git.bthlabs.pl/tomekwojcik/bthlabs-jsonrpc/ |
+-------------------+------------------------------------------------------+

View File

@@ -0,0 +1,89 @@
Overview
========
This section provides the general overview of the library.
Installation
------------
.. code-block:: shell
$ pip install bthlabs_jsonrpc_core
Usage
-----
While this package is built to mostly support other integrations, it's possible
to use it directly to add a JSONRPC endpoint to an existing Web app.
Consider the following Flask app:
.. code-block:: python
from bthlabs_jsonrpc_core import Executor, register_method
from flask import Flask, jsonify, request
app = Flask(__name__)
@register_method('hello')
def hello(who='World'):
return f'Hello, {who}!'
@app.route('/rpc', methods=['POST'])
def post_rpc():
executor = Executor()
serializer = executor.execute(request.get_data())
return jsonify(serializer.data)
This application will allow calling the ``hello`` JSONPRC method via the
``POST /rpc`` endpoint. This approach is limited, as it doesn't provide the
means of performing any access control and other checks, leaving the app to
do this. In practice, it's best to rely on framework integrations.
Calling Conventions
-------------------
The JSONRPC 2.0 spec calls for two conventions for passing method parameters -
*by-position* (using an array) or *by-name* (using a JSON object). BTHLabs
JSONRPC implements both.
The ``hello`` method from the Flask app example could be called using the
following payloads.
.. code-block:: json
{
"jsonrpc": "2.0",
"id": "hello"
}
This payload would call the method without arguments. In this case, it would
return ``Hello, World!``.
.. code-block:: json
{
"jsonrpc": "2.0",
"id": "hello",
"params": ["JSONRPC"]
}
This payload would call the method with one positional argument. In this case,
it would return ``Hello, JSONRPC!``.
.. code-block:: json
{
"jsonrpc": "2.0",
"id": "hello",
"params": {"who": "JSONRPC"}
}
This payload would call the method with one keyword argument. In this case,
it would return ``Hello, JSONRPC!``.
While writing your methods, you should consider these conventions and specify
your method signatures accordingly.

672
packages/bthlabs-jsonrpc-core/poetry.lock generated Normal file
View File

@@ -0,0 +1,672 @@
[[package]]
name = "alabaster"
version = "0.7.12"
description = "A configurable sidebar-enabled Sphinx theme"
category = "dev"
optional = false
python-versions = "*"
[[package]]
name = "atomicwrites"
version = "1.4.0"
description = "Atomic file writes."
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[package]]
name = "attrs"
version = "21.4.0"
description = "Classes Without Boilerplate"
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[package.extras]
dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"]
docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"]
tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "cloudpickle"]
tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "cloudpickle"]
[[package]]
name = "babel"
version = "2.10.1"
description = "Internationalization utilities"
category = "dev"
optional = false
python-versions = ">=3.6"
[package.dependencies]
pytz = ">=2015.7"
[[package]]
name = "certifi"
version = "2022.5.18.1"
description = "Python package for providing Mozilla's CA Bundle."
category = "dev"
optional = false
python-versions = ">=3.6"
[[package]]
name = "charset-normalizer"
version = "2.0.12"
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
category = "dev"
optional = false
python-versions = ">=3.5.0"
[package.extras]
unicode_backport = ["unicodedata2"]
[[package]]
name = "colorama"
version = "0.4.4"
description = "Cross-platform colored terminal text."
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[[package]]
name = "docutils"
version = "0.17.1"
description = "Docutils -- Python Documentation Utilities"
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[[package]]
name = "flake8"
version = "4.0.1"
description = "the modular source code checker: pep8 pyflakes and co"
category = "dev"
optional = false
python-versions = ">=3.6"
[package.dependencies]
mccabe = ">=0.6.0,<0.7.0"
pycodestyle = ">=2.8.0,<2.9.0"
pyflakes = ">=2.4.0,<2.5.0"
[[package]]
name = "flake8-commas"
version = "2.1.0"
description = "Flake8 lint for trailing commas."
category = "dev"
optional = false
python-versions = "*"
[package.dependencies]
flake8 = ">=2"
[[package]]
name = "idna"
version = "3.3"
description = "Internationalized Domain Names in Applications (IDNA)"
category = "dev"
optional = false
python-versions = ">=3.5"
[[package]]
name = "imagesize"
version = "1.3.0"
description = "Getting image size from png/jpeg/jpeg2000/gif file"
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[package]]
name = "iniconfig"
version = "1.1.1"
description = "iniconfig: brain-dead simple config-ini parsing"
category = "dev"
optional = false
python-versions = "*"
[[package]]
name = "jinja2"
version = "3.1.2"
description = "A very fast and expressive template engine."
category = "dev"
optional = false
python-versions = ">=3.7"
[package.dependencies]
MarkupSafe = ">=2.0"
[package.extras]
i18n = ["Babel (>=2.7)"]
[[package]]
name = "markupsafe"
version = "2.1.1"
description = "Safely add untrusted strings to HTML/XML markup."
category = "dev"
optional = false
python-versions = ">=3.7"
[[package]]
name = "mccabe"
version = "0.6.1"
description = "McCabe checker, plugin for flake8"
category = "dev"
optional = false
python-versions = "*"
[[package]]
name = "mypy"
version = "0.950"
description = "Optional static typing for Python"
category = "dev"
optional = false
python-versions = ">=3.6"
[package.dependencies]
mypy-extensions = ">=0.4.3"
tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
typing-extensions = ">=3.10"
[package.extras]
dmypy = ["psutil (>=4.0)"]
python2 = ["typed-ast (>=1.4.0,<2)"]
reports = ["lxml"]
[[package]]
name = "mypy-extensions"
version = "0.4.3"
description = "Experimental type system extensions for programs checked with the mypy typechecker."
category = "dev"
optional = false
python-versions = "*"
[[package]]
name = "packaging"
version = "21.3"
description = "Core utilities for Python packages"
category = "dev"
optional = false
python-versions = ">=3.6"
[package.dependencies]
pyparsing = ">=2.0.2,<3.0.5 || >3.0.5"
[[package]]
name = "pluggy"
version = "1.0.0"
description = "plugin and hook calling mechanisms for python"
category = "dev"
optional = false
python-versions = ">=3.6"
[package.extras]
dev = ["pre-commit", "tox"]
testing = ["pytest", "pytest-benchmark"]
[[package]]
name = "py"
version = "1.11.0"
description = "library with cross-python path, ini-parsing, io, code, log facilities"
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[[package]]
name = "pycodestyle"
version = "2.8.0"
description = "Python style guide checker"
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[[package]]
name = "pyflakes"
version = "2.4.0"
description = "passive checker of Python programs"
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[package]]
name = "pygments"
version = "2.12.0"
description = "Pygments is a syntax highlighting package written in Python."
category = "dev"
optional = false
python-versions = ">=3.6"
[[package]]
name = "pyparsing"
version = "3.0.9"
description = "pyparsing module - Classes and methods to define and execute parsing grammars"
category = "dev"
optional = false
python-versions = ">=3.6.8"
[package.extras]
diagrams = ["railroad-diagrams", "jinja2"]
[[package]]
name = "pytest"
version = "7.1.2"
description = "pytest: simple powerful testing with Python"
category = "dev"
optional = false
python-versions = ">=3.7"
[package.dependencies]
atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""}
attrs = ">=19.2.0"
colorama = {version = "*", markers = "sys_platform == \"win32\""}
iniconfig = "*"
packaging = "*"
pluggy = ">=0.12,<2.0"
py = ">=1.8.2"
tomli = ">=1.0.0"
[package.extras]
testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"]
[[package]]
name = "pytz"
version = "2022.1"
description = "World timezone definitions, modern and historical"
category = "dev"
optional = false
python-versions = "*"
[[package]]
name = "requests"
version = "2.27.1"
description = "Python HTTP for Humans."
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
[package.dependencies]
certifi = ">=2017.4.17"
charset-normalizer = {version = ">=2.0.0,<2.1.0", markers = "python_version >= \"3\""}
idna = {version = ">=2.5,<4", markers = "python_version >= \"3\""}
urllib3 = ">=1.21.1,<1.27"
[package.extras]
socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"]
use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"]
[[package]]
name = "snowballstemmer"
version = "2.2.0"
description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms."
category = "dev"
optional = false
python-versions = "*"
[[package]]
name = "sphinx"
version = "4.5.0"
description = "Python documentation generator"
category = "dev"
optional = false
python-versions = ">=3.6"
[package.dependencies]
alabaster = ">=0.7,<0.8"
babel = ">=1.3"
colorama = {version = ">=0.3.5", markers = "sys_platform == \"win32\""}
docutils = ">=0.14,<0.18"
imagesize = "*"
Jinja2 = ">=2.3"
packaging = "*"
Pygments = ">=2.0"
requests = ">=2.5.0"
snowballstemmer = ">=1.1"
sphinxcontrib-applehelp = "*"
sphinxcontrib-devhelp = "*"
sphinxcontrib-htmlhelp = ">=2.0.0"
sphinxcontrib-jsmath = "*"
sphinxcontrib-qthelp = "*"
sphinxcontrib-serializinghtml = ">=1.1.5"
[package.extras]
docs = ["sphinxcontrib-websupport"]
lint = ["flake8 (>=3.5.0)", "isort", "mypy (>=0.931)", "docutils-stubs", "types-typed-ast", "types-requests"]
test = ["pytest", "pytest-cov", "html5lib", "cython", "typed-ast"]
[[package]]
name = "sphinx-rtd-theme"
version = "1.0.0"
description = "Read the Docs theme for Sphinx"
category = "dev"
optional = false
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*"
[package.dependencies]
docutils = "<0.18"
sphinx = ">=1.6"
[package.extras]
dev = ["transifex-client", "sphinxcontrib-httpdomain", "bump2version"]
[[package]]
name = "sphinxcontrib-applehelp"
version = "1.0.2"
description = "sphinxcontrib-applehelp is a sphinx extension which outputs Apple help books"
category = "dev"
optional = false
python-versions = ">=3.5"
[package.extras]
lint = ["flake8", "mypy", "docutils-stubs"]
test = ["pytest"]
[[package]]
name = "sphinxcontrib-devhelp"
version = "1.0.2"
description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp document."
category = "dev"
optional = false
python-versions = ">=3.5"
[package.extras]
lint = ["flake8", "mypy", "docutils-stubs"]
test = ["pytest"]
[[package]]
name = "sphinxcontrib-htmlhelp"
version = "2.0.0"
description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files"
category = "dev"
optional = false
python-versions = ">=3.6"
[package.extras]
lint = ["flake8", "mypy", "docutils-stubs"]
test = ["pytest", "html5lib"]
[[package]]
name = "sphinxcontrib-jsmath"
version = "1.0.1"
description = "A sphinx extension which renders display math in HTML via JavaScript"
category = "dev"
optional = false
python-versions = ">=3.5"
[package.extras]
test = ["pytest", "flake8", "mypy"]
[[package]]
name = "sphinxcontrib-qthelp"
version = "1.0.3"
description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp document."
category = "dev"
optional = false
python-versions = ">=3.5"
[package.extras]
lint = ["flake8", "mypy", "docutils-stubs"]
test = ["pytest"]
[[package]]
name = "sphinxcontrib-serializinghtml"
version = "1.1.5"
description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)."
category = "dev"
optional = false
python-versions = ">=3.5"
[package.extras]
lint = ["flake8", "mypy", "docutils-stubs"]
test = ["pytest"]
[[package]]
name = "tomli"
version = "2.0.1"
description = "A lil' TOML parser"
category = "dev"
optional = false
python-versions = ">=3.7"
[[package]]
name = "typing-extensions"
version = "4.2.0"
description = "Backported and Experimental Type Hints for Python 3.7+"
category = "dev"
optional = false
python-versions = ">=3.7"
[[package]]
name = "urllib3"
version = "1.26.9"
description = "HTTP library with thread-safe connection pooling, file post, and more."
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4"
[package.extras]
brotli = ["brotlicffi (>=0.8.0)", "brotli (>=1.0.9)", "brotlipy (>=0.6.0)"]
secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"]
socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
[metadata]
lock-version = "1.1"
python-versions = "^3.10"
content-hash = "8fc33f34c2fb6ae94096a34959130a45e8ab037a3e76d7b9cb3790ca518d902d"
[metadata.files]
alabaster = [
{file = "alabaster-0.7.12-py2.py3-none-any.whl", hash = "sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359"},
{file = "alabaster-0.7.12.tar.gz", hash = "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"},
]
atomicwrites = [
{file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"},
{file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"},
]
attrs = [
{file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"},
{file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"},
]
babel = [
{file = "Babel-2.10.1-py3-none-any.whl", hash = "sha256:3f349e85ad3154559ac4930c3918247d319f21910d5ce4b25d439ed8693b98d2"},
{file = "Babel-2.10.1.tar.gz", hash = "sha256:98aeaca086133efb3e1e2aad0396987490c8425929ddbcfe0550184fdc54cd13"},
]
certifi = [
{file = "certifi-2022.5.18.1-py3-none-any.whl", hash = "sha256:f1d53542ee8cbedbe2118b5686372fb33c297fcd6379b050cca0ef13a597382a"},
{file = "certifi-2022.5.18.1.tar.gz", hash = "sha256:9c5705e395cd70084351dd8ad5c41e65655e08ce46f2ec9cf6c2c08390f71eb7"},
]
charset-normalizer = [
{file = "charset-normalizer-2.0.12.tar.gz", hash = "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597"},
{file = "charset_normalizer-2.0.12-py3-none-any.whl", hash = "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"},
]
colorama = [
{file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"},
{file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"},
]
docutils = [
{file = "docutils-0.17.1-py2.py3-none-any.whl", hash = "sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61"},
{file = "docutils-0.17.1.tar.gz", hash = "sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125"},
]
flake8 = [
{file = "flake8-4.0.1-py2.py3-none-any.whl", hash = "sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d"},
{file = "flake8-4.0.1.tar.gz", hash = "sha256:806e034dda44114815e23c16ef92f95c91e4c71100ff52813adf7132a6ad870d"},
]
flake8-commas = [
{file = "flake8-commas-2.1.0.tar.gz", hash = "sha256:940441ab8ee544df564ae3b3f49f20462d75d5c7cac2463e0b27436e2050f263"},
{file = "flake8_commas-2.1.0-py2.py3-none-any.whl", hash = "sha256:ebb96c31e01d0ef1d0685a21f3f0e2f8153a0381430e748bf0bbbb5d5b453d54"},
]
idna = [
{file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"},
{file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"},
]
imagesize = [
{file = "imagesize-1.3.0-py2.py3-none-any.whl", hash = "sha256:1db2f82529e53c3e929e8926a1fa9235aa82d0bd0c580359c67ec31b2fddaa8c"},
{file = "imagesize-1.3.0.tar.gz", hash = "sha256:cd1750d452385ca327479d45b64d9c7729ecf0b3969a58148298c77092261f9d"},
]
iniconfig = [
{file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
{file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
]
jinja2 = [
{file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"},
{file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"},
]
markupsafe = [
{file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812"},
{file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a"},
{file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e"},
{file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10c1bfff05d95783da83491be968e8fe789263689c02724e0c691933c52994f5"},
{file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7bd98b796e2b6553da7225aeb61f447f80a1ca64f41d83612e6139ca5213aa4"},
{file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b09bf97215625a311f669476f44b8b318b075847b49316d3e28c08e41a7a573f"},
{file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:694deca8d702d5db21ec83983ce0bb4b26a578e71fbdbd4fdcd387daa90e4d5e"},
{file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:efc1913fd2ca4f334418481c7e595c00aad186563bbc1ec76067848c7ca0a933"},
{file = "MarkupSafe-2.1.1-cp310-cp310-win32.whl", hash = "sha256:4a33dea2b688b3190ee12bd7cfa29d39c9ed176bda40bfa11099a3ce5d3a7ac6"},
{file = "MarkupSafe-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:dda30ba7e87fbbb7eab1ec9f58678558fd9a6b8b853530e176eabd064da81417"},
{file = "MarkupSafe-2.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:671cd1187ed5e62818414afe79ed29da836dde67166a9fac6d435873c44fdd02"},
{file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3799351e2336dc91ea70b034983ee71cf2f9533cdff7c14c90ea126bfd95d65a"},
{file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e72591e9ecd94d7feb70c1cbd7be7b3ebea3f548870aa91e2732960fa4d57a37"},
{file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6fbf47b5d3728c6aea2abb0589b5d30459e369baa772e0f37a0320185e87c980"},
{file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d5ee4f386140395a2c818d149221149c54849dfcfcb9f1debfe07a8b8bd63f9a"},
{file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:bcb3ed405ed3222f9904899563d6fc492ff75cce56cba05e32eff40e6acbeaa3"},
{file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e1c0b87e09fa55a220f058d1d49d3fb8df88fbfab58558f1198e08c1e1de842a"},
{file = "MarkupSafe-2.1.1-cp37-cp37m-win32.whl", hash = "sha256:8dc1c72a69aa7e082593c4a203dcf94ddb74bb5c8a731e4e1eb68d031e8498ff"},
{file = "MarkupSafe-2.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:97a68e6ada378df82bc9f16b800ab77cbf4b2fada0081794318520138c088e4a"},
{file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e8c843bbcda3a2f1e3c2ab25913c80a3c5376cd00c6e8c4a86a89a28c8dc5452"},
{file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0212a68688482dc52b2d45013df70d169f542b7394fc744c02a57374a4207003"},
{file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e576a51ad59e4bfaac456023a78f6b5e6e7651dcd383bcc3e18d06f9b55d6d1"},
{file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b9fe39a2ccc108a4accc2676e77da025ce383c108593d65cc909add5c3bd601"},
{file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96e37a3dc86e80bf81758c152fe66dbf60ed5eca3d26305edf01892257049925"},
{file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6d0072fea50feec76a4c418096652f2c3238eaa014b2f94aeb1d56a66b41403f"},
{file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:089cf3dbf0cd6c100f02945abeb18484bd1ee57a079aefd52cffd17fba910b88"},
{file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6a074d34ee7a5ce3effbc526b7083ec9731bb3cbf921bbe1d3005d4d2bdb3a63"},
{file = "MarkupSafe-2.1.1-cp38-cp38-win32.whl", hash = "sha256:421be9fbf0ffe9ffd7a378aafebbf6f4602d564d34be190fc19a193232fd12b1"},
{file = "MarkupSafe-2.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc7b548b17d238737688817ab67deebb30e8073c95749d55538ed473130ec0c7"},
{file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e04e26803c9c3851c931eac40c695602c6295b8d432cbe78609649ad9bd2da8a"},
{file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b87db4360013327109564f0e591bd2a3b318547bcef31b468a92ee504d07ae4f"},
{file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99a2a507ed3ac881b975a2976d59f38c19386d128e7a9a18b7df6fff1fd4c1d6"},
{file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56442863ed2b06d19c37f94d999035e15ee982988920e12a5b4ba29b62ad1f77"},
{file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ce11ee3f23f79dbd06fb3d63e2f6af7b12db1d46932fe7bd8afa259a5996603"},
{file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:33b74d289bd2f5e527beadcaa3f401e0df0a89927c1559c8566c066fa4248ab7"},
{file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:43093fb83d8343aac0b1baa75516da6092f58f41200907ef92448ecab8825135"},
{file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8e3dcf21f367459434c18e71b2a9532d96547aef8a871872a5bd69a715c15f96"},
{file = "MarkupSafe-2.1.1-cp39-cp39-win32.whl", hash = "sha256:d4306c36ca495956b6d568d276ac11fdd9c30a36f1b6eb928070dc5360b22e1c"},
{file = "MarkupSafe-2.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247"},
{file = "MarkupSafe-2.1.1.tar.gz", hash = "sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b"},
]
mccabe = [
{file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"},
{file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"},
]
mypy = [
{file = "mypy-0.950-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cf9c261958a769a3bd38c3e133801ebcd284ffb734ea12d01457cb09eacf7d7b"},
{file = "mypy-0.950-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b5b5bd0ffb11b4aba2bb6d31b8643902c48f990cc92fda4e21afac658044f0c0"},
{file = "mypy-0.950-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5e7647df0f8fc947388e6251d728189cfadb3b1e558407f93254e35abc026e22"},
{file = "mypy-0.950-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:eaff8156016487c1af5ffa5304c3e3fd183edcb412f3e9c72db349faf3f6e0eb"},
{file = "mypy-0.950-cp310-cp310-win_amd64.whl", hash = "sha256:563514c7dc504698fb66bb1cf897657a173a496406f1866afae73ab5b3cdb334"},
{file = "mypy-0.950-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:dd4d670eee9610bf61c25c940e9ade2d0ed05eb44227275cce88701fee014b1f"},
{file = "mypy-0.950-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ca75ecf2783395ca3016a5e455cb322ba26b6d33b4b413fcdedfc632e67941dc"},
{file = "mypy-0.950-cp36-cp36m-win_amd64.whl", hash = "sha256:6003de687c13196e8a1243a5e4bcce617d79b88f83ee6625437e335d89dfebe2"},
{file = "mypy-0.950-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4c653e4846f287051599ed8f4b3c044b80e540e88feec76b11044ddc5612ffed"},
{file = "mypy-0.950-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e19736af56947addedce4674c0971e5dceef1b5ec7d667fe86bcd2b07f8f9075"},
{file = "mypy-0.950-cp37-cp37m-win_amd64.whl", hash = "sha256:ef7beb2a3582eb7a9f37beaf38a28acfd801988cde688760aea9e6cc4832b10b"},
{file = "mypy-0.950-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0112752a6ff07230f9ec2f71b0d3d4e088a910fdce454fdb6553e83ed0eced7d"},
{file = "mypy-0.950-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ee0a36edd332ed2c5208565ae6e3a7afc0eabb53f5327e281f2ef03a6bc7687a"},
{file = "mypy-0.950-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:77423570c04aca807508a492037abbd72b12a1fb25a385847d191cd50b2c9605"},
{file = "mypy-0.950-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5ce6a09042b6da16d773d2110e44f169683d8cc8687e79ec6d1181a72cb028d2"},
{file = "mypy-0.950-cp38-cp38-win_amd64.whl", hash = "sha256:5b231afd6a6e951381b9ef09a1223b1feabe13625388db48a8690f8daa9b71ff"},
{file = "mypy-0.950-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:0384d9f3af49837baa92f559d3fa673e6d2652a16550a9ee07fc08c736f5e6f8"},
{file = "mypy-0.950-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1fdeb0a0f64f2a874a4c1f5271f06e40e1e9779bf55f9567f149466fc7a55038"},
{file = "mypy-0.950-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:61504b9a5ae166ba5ecfed9e93357fd51aa693d3d434b582a925338a2ff57fd2"},
{file = "mypy-0.950-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a952b8bc0ae278fc6316e6384f67bb9a396eb30aced6ad034d3a76120ebcc519"},
{file = "mypy-0.950-cp39-cp39-win_amd64.whl", hash = "sha256:eaea21d150fb26d7b4856766e7addcf929119dd19fc832b22e71d942835201ef"},
{file = "mypy-0.950-py3-none-any.whl", hash = "sha256:a4d9898f46446bfb6405383b57b96737dcfd0a7f25b748e78ef3e8c576bba3cb"},
{file = "mypy-0.950.tar.gz", hash = "sha256:1b333cfbca1762ff15808a0ef4f71b5d3eed8528b23ea1c3fb50543c867d68de"},
]
mypy-extensions = [
{file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"},
{file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"},
]
packaging = [
{file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"},
{file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"},
]
pluggy = [
{file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
{file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
]
py = [
{file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"},
{file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"},
]
pycodestyle = [
{file = "pycodestyle-2.8.0-py2.py3-none-any.whl", hash = "sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20"},
{file = "pycodestyle-2.8.0.tar.gz", hash = "sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f"},
]
pyflakes = [
{file = "pyflakes-2.4.0-py2.py3-none-any.whl", hash = "sha256:3bb3a3f256f4b7968c9c788781e4ff07dce46bdf12339dcda61053375426ee2e"},
{file = "pyflakes-2.4.0.tar.gz", hash = "sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c"},
]
pygments = [
{file = "Pygments-2.12.0-py3-none-any.whl", hash = "sha256:dc9c10fb40944260f6ed4c688ece0cd2048414940f1cea51b8b226318411c519"},
{file = "Pygments-2.12.0.tar.gz", hash = "sha256:5eb116118f9612ff1ee89ac96437bb6b49e8f04d8a13b514ba26f620208e26eb"},
]
pyparsing = [
{file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"},
{file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"},
]
pytest = [
{file = "pytest-7.1.2-py3-none-any.whl", hash = "sha256:13d0e3ccfc2b6e26be000cb6568c832ba67ba32e719443bfe725814d3c42433c"},
{file = "pytest-7.1.2.tar.gz", hash = "sha256:a06a0425453864a270bc45e71f783330a7428defb4230fb5e6a731fde06ecd45"},
]
pytz = [
{file = "pytz-2022.1-py2.py3-none-any.whl", hash = "sha256:e68985985296d9a66a881eb3193b0906246245294a881e7c8afe623866ac6a5c"},
{file = "pytz-2022.1.tar.gz", hash = "sha256:1e760e2fe6a8163bc0b3d9a19c4f84342afa0a2affebfaa84b01b978a02ecaa7"},
]
requests = [
{file = "requests-2.27.1-py2.py3-none-any.whl", hash = "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"},
{file = "requests-2.27.1.tar.gz", hash = "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61"},
]
snowballstemmer = [
{file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"},
{file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"},
]
sphinx = [
{file = "Sphinx-4.5.0-py3-none-any.whl", hash = "sha256:ebf612653238bcc8f4359627a9b7ce44ede6fdd75d9d30f68255c7383d3a6226"},
{file = "Sphinx-4.5.0.tar.gz", hash = "sha256:7bf8ca9637a4ee15af412d1a1d9689fec70523a68ca9bb9127c2f3eeb344e2e6"},
]
sphinx-rtd-theme = [
{file = "sphinx_rtd_theme-1.0.0-py2.py3-none-any.whl", hash = "sha256:4d35a56f4508cfee4c4fb604373ede6feae2a306731d533f409ef5c3496fdbd8"},
{file = "sphinx_rtd_theme-1.0.0.tar.gz", hash = "sha256:eec6d497e4c2195fa0e8b2016b337532b8a699a68bcb22a512870e16925c6a5c"},
]
sphinxcontrib-applehelp = [
{file = "sphinxcontrib-applehelp-1.0.2.tar.gz", hash = "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58"},
{file = "sphinxcontrib_applehelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a"},
]
sphinxcontrib-devhelp = [
{file = "sphinxcontrib-devhelp-1.0.2.tar.gz", hash = "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4"},
{file = "sphinxcontrib_devhelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e"},
]
sphinxcontrib-htmlhelp = [
{file = "sphinxcontrib-htmlhelp-2.0.0.tar.gz", hash = "sha256:f5f8bb2d0d629f398bf47d0d69c07bc13b65f75a81ad9e2f71a63d4b7a2f6db2"},
{file = "sphinxcontrib_htmlhelp-2.0.0-py2.py3-none-any.whl", hash = "sha256:d412243dfb797ae3ec2b59eca0e52dac12e75a241bf0e4eb861e450d06c6ed07"},
]
sphinxcontrib-jsmath = [
{file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"},
{file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"},
]
sphinxcontrib-qthelp = [
{file = "sphinxcontrib-qthelp-1.0.3.tar.gz", hash = "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72"},
{file = "sphinxcontrib_qthelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6"},
]
sphinxcontrib-serializinghtml = [
{file = "sphinxcontrib-serializinghtml-1.1.5.tar.gz", hash = "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952"},
{file = "sphinxcontrib_serializinghtml-1.1.5-py2.py3-none-any.whl", hash = "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd"},
]
tomli = [
{file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
]
typing-extensions = [
{file = "typing_extensions-4.2.0-py3-none-any.whl", hash = "sha256:6657594ee297170d19f67d55c05852a874e7eb634f4f753dbd667855e07c1708"},
{file = "typing_extensions-4.2.0.tar.gz", hash = "sha256:f1c24655a0da0d1b67f07e17a5e6b2a105894e6824b92096378bb3668ef02376"},
]
urllib3 = [
{file = "urllib3-1.26.9-py2.py3-none-any.whl", hash = "sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14"},
{file = "urllib3-1.26.9.tar.gz", hash = "sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e"},
]

View File

@@ -0,0 +1,3 @@
[virtualenvs]
create = true
in-project = true

View File

@@ -0,0 +1,26 @@
[tool.poetry]
name = "bthlabs-jsonrpc-core"
version = "1.0.0"
description = "BTHLabs JSONRPC - Core"
authors = ["Tomek Wójcik <contact@bthlabs.pl>"]
maintainers = ["BTHLabs <contact@bthlabs.pl>"]
license = "MIT License"
readme = "README.rst"
homepage = "https://projects.bthlabs.pl/bthlabs-jsonrpc/"
repository = "https://git.bthlabs.pl/tomekwojcik/bthlabs-jsonrpc/"
documentation = "https://projects.bthlabs.pl/bthlabs-jsonrpc/core/"
[tool.poetry.dependencies]
python = "^3.10"
[tool.poetry.dev-dependencies]
flake8 = "4.0.1"
flake8-commas = "2.1.0"
mypy = "0.950"
pytest = "7.1.2"
sphinx = "4.5.0"
sphinx-rtd-theme = "1.0.0"
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"

View File

@@ -0,0 +1,4 @@
[flake8]
exclude = .venv/,.pytest_cache/
ignore = E402
max-line-length = 119

View File

@@ -0,0 +1,2 @@
export VIRTUAL_ENV="`realpath .venv`"
export PATH="$VIRTUAL_ENV/bin:$PATH"

View File

@@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
from unittest import mock
import pytest
from bthlabs_jsonrpc_core.registry import MethodRegistry
from bthlabs_jsonrpc_core.serializer import JSONRPCSerializer
@pytest.fixture
def fake_method_registry():
return mock.Mock(spec=MethodRegistry)
@pytest.fixture
def fake_handler():
return mock.Mock()
@pytest.fixture
def fake_rpc_serializer():
return mock.Mock(spec=JSONRPCSerializer)

View File

@@ -0,0 +1,53 @@
# -*- coding: utf-8 -*-
from unittest import mock
from bthlabs_jsonrpc_core import decorators
from bthlabs_jsonrpc_core.registry import MethodRegistry
@mock.patch.object(decorators.MethodRegistry, 'shared_registry')
def test_default_namespace(mock_shared_registry,
fake_method_registry,
fake_handler):
# Given
mock_shared_registry.return_value = fake_method_registry
decorator = decorators.register_method('test')
# When
result = decorator(fake_handler)
# Then
assert result is fake_handler
assert result.jsonrpc_method == 'test'
assert result.jsonrpc_namespace == MethodRegistry.DEFAULT_NAMESPACE
assert mock_shared_registry.called is True
fake_method_registry.register_method.assert_called_with(
MethodRegistry.DEFAULT_NAMESPACE, 'test', fake_handler,
)
@mock.patch.object(decorators.MethodRegistry, 'shared_registry')
def test_custom_namespace(mock_shared_registry,
fake_method_registry,
fake_handler):
# Given
mock_shared_registry.return_value = fake_method_registry
decorator = decorators.register_method('test', namespace='testing')
# When
result = decorator(fake_handler)
# Then
assert result is fake_handler
assert result.jsonrpc_method == 'test'
assert result.jsonrpc_namespace == 'testing'
assert mock_shared_registry.called is True
fake_method_registry.register_method.assert_called_with(
'testing', 'test', fake_handler,
)

View File

@@ -0,0 +1,32 @@
# -*- coding: utf-8 -*-
from bthlabs_jsonrpc_core import exceptions
def test_to_rpc():
# Given
exception = exceptions.BaseJSONRPCError(data='spam')
exception.ERROR_CODE = -32604
exception.ERROR_MESSAGE = 'Test error'
# When
result = exception.to_rpc()
# Then
assert result['code'] == exception.ERROR_CODE
assert result['message'] == exception.ERROR_MESSAGE
assert result['data'] == exception.data
def test_to_rpc_without_data():
# Given
exception = exceptions.BaseJSONRPCError()
exception.ERROR_CODE = -32604
exception.ERROR_MESSAGE = 'Test error'
# When
result = exception.to_rpc()
# Then
assert result['code'] == exception.ERROR_CODE
assert result['message'] == exception.ERROR_MESSAGE
assert 'data' not in result

View File

@@ -0,0 +1,776 @@
# -*- coding: utf-8 -*-
import json
from unittest import mock
import pytest
from bthlabs_jsonrpc_core import exceptions, executor
from bthlabs_jsonrpc_core.registry import MethodRegistry
from bthlabs_jsonrpc_core.serializer import JSONRPCSerializer
@pytest.fixture
def single_call():
return {
'jsonrpc': '2.0',
'id': 'test',
'method': 'test',
}
@pytest.fixture
def batch_calls():
return [
{
'jsonrpc': '2.0',
'id': 'test',
'method': 'test',
},
{
'jsonrpc': '2.0',
'id': 'test2',
'method': 'test',
},
]
@pytest.fixture
def jsonrpc_error():
return exceptions.BaseJSONRPCError('I HAZ FIAL')
@pytest.fixture
def execute_context():
return executor.Executor.ExecuteContext([])
def test_CallContext_invalid_context():
# When
result = executor.Executor.CallContext.invalid_context()
# Then
assert result.method is None
assert result.handler is None
assert result.args is None
assert result.kwargs is None
def test_CallContext_is_valid_method_none(fake_handler):
# When
call_context = executor.Executor.CallContext(None, fake_handler, [], {})
# Then
assert call_context.is_valid is False
def test_CallContext_is_valid_handler_none():
# When
call_context = executor.Executor.CallContext('test', None, [], {})
# Then
assert call_context.is_valid is False
def test_CallContext_is_valid_args_none(fake_handler):
# When
call_context = executor.Executor.CallContext(
'test', fake_handler, None, {},
)
# Then
assert call_context.is_valid is False
def test_CallContext_is_valid_kwargs_none(fake_handler):
# When
call_context = executor.Executor.CallContext(
'test', fake_handler, [], None,
)
# Then
assert call_context.is_valid is False
def test_CallContext_is_valid(fake_handler):
# When
call_context = executor.Executor.CallContext('test', fake_handler, [], {})
# Then
assert call_context.is_valid is True
def test_init_default_namespace():
# When
result = executor.Executor()
# Then
assert result.namespace == MethodRegistry.DEFAULT_NAMESPACE
def test_init_custom_namespace():
# When
result = executor.Executor(namespace='testing')
# Then
assert result.namespace == 'testing'
@mock.patch.object(executor.MethodRegistry, 'shared_registry')
def test_list_methods(mock_shared_registry, fake_method_registry):
# Given
fake_method_registry.get_methods.return_value = ['test']
mock_shared_registry.return_value = fake_method_registry
the_executor = executor.Executor()
# When
result = the_executor.list_methods()
# Then
assert result == ['system.list_methods', 'test']
assert mock_shared_registry.called is True
fake_method_registry.get_methods.assert_called_with(the_executor.namespace)
def test_get_internal_handler_list_methods():
# Given
the_executor = executor.Executor()
# When
result = the_executor.get_internal_handler('system.list_methods')
# Then
assert result == the_executor.list_methods
def test_get_internal_handler_method_not_found():
# Given
the_executor = executor.Executor()
# When
try:
_ = the_executor.get_internal_handler('test')
except Exception as exception:
# Then
assert isinstance(exception, exceptions.JSONRPCMethodNotFoundError)
else:
assert False, 'No exception raised?'
def test_deserialize_data():
# Given
the_executor = executor.Executor()
# When
result = the_executor.deserialize_data('"spam"')
# Then
assert result == 'spam'
def test_deserialize_data_error():
# Given
the_executor = executor.Executor()
# When
try:
_ = the_executor.deserialize_data(None)
except Exception as exception:
# Then
assert isinstance(exception, exceptions.JSONRPCParseError)
else:
assert False, 'No exception raised?'
def test_get_calls_batch(batch_calls):
# Given
the_executor = executor.Executor()
# When
result = the_executor.get_calls(batch_calls)
# Then
assert result == batch_calls
def test_get_calls_single(single_call):
# Given
the_executor = executor.Executor()
# When
result = the_executor.get_calls(single_call)
# Then
assert result == [single_call]
def test_get_calls_empty():
# Given
the_executor = executor.Executor()
# When
try:
_ = the_executor.get_calls([])
except Exception as exception:
# Then
assert isinstance(exception, exceptions.JSONRPCInvalidRequestError)
else:
assert False, 'No exception raised?'
def test_get_call_spec_not_dict():
# Given
the_executor = executor.Executor()
# When
try:
_ = the_executor.get_call_spec(None)
except Exception as exception:
# Then
assert isinstance(exception, exceptions.JSONRPCInvalidRequestError)
else:
assert False, 'No exception raised?'
def test_get_call_spec_wihtout_jsonrpc(single_call):
# Given
single_call.pop('jsonrpc')
the_executor = executor.Executor()
# When
try:
_ = the_executor.get_call_spec(single_call)
except Exception as exception:
# Then
assert isinstance(exception, exceptions.JSONRPCInvalidRequestError)
else:
assert False, 'No exception raised?'
def test_get_call_spec_invalid_jsonrpc(single_call):
# Given
single_call['jsonrpc'] = 'test'
the_executor = executor.Executor()
# When
try:
_ = the_executor.get_call_spec(single_call)
except Exception as exception:
# Then
assert isinstance(exception, exceptions.JSONRPCInvalidRequestError)
else:
assert False, 'No exception raised?'
def test_get_call_spec_wihtout_method(single_call):
# Given
single_call.pop('method')
the_executor = executor.Executor()
# When
try:
_ = the_executor.get_call_spec(single_call)
except Exception as exception:
# Then
assert isinstance(exception, exceptions.JSONRPCInvalidRequestError)
else:
assert False, 'No exception raised?'
@mock.patch.object(executor.MethodRegistry, 'shared_registry')
def test_get_call_spec_internal_method(mock_shared_registry,
single_call,
fake_handler):
# Given
single_call['method'] = 'system.list_methods'
the_executor = executor.Executor()
with mock.patch.object(the_executor, 'get_internal_handler') as mock_get_internal_handler:
mock_get_internal_handler.return_value = fake_handler
# When
result = the_executor.get_call_spec(single_call)
# Then
assert result[0] == 'system.list_methods'
assert result[1] is fake_handler
mock_get_internal_handler.assert_called_with('system.list_methods')
assert mock_shared_registry.called is False
@mock.patch.object(executor.MethodRegistry, 'shared_registry')
def test_get_call_spec_registry_method(mock_shared_registry,
single_call,
fake_method_registry,
fake_handler):
# Given
fake_method_registry.get_handler.return_value = fake_handler
mock_shared_registry.return_value = fake_method_registry
the_executor = executor.Executor()
with mock.patch.object(the_executor, 'get_internal_handler') as mock_get_internal_handler:
# When
result = the_executor.get_call_spec(single_call)
# Then
assert result[0] == 'test'
assert result[1] is fake_handler
assert mock_get_internal_handler.called is False
fake_method_registry.get_handler.assert_called_with(
the_executor.namespace, 'test',
)
@mock.patch.object(executor.MethodRegistry, 'shared_registry')
def test_get_call_spec_method_not_found(mock_shared_registry,
single_call,
fake_method_registry):
# Given
fake_method_registry.get_handler.return_value = None
mock_shared_registry.return_value = fake_method_registry
the_executor = executor.Executor()
# When
try:
_ = the_executor.get_call_spec(single_call)
except Exception as exception:
# Then
assert isinstance(exception, exceptions.JSONRPCMethodNotFoundError)
else:
assert False, 'No exception raised?'
@mock.patch.object(executor.MethodRegistry, 'shared_registry')
def test_get_call_spec_invalid_params(mock_shared_registry,
single_call,
fake_method_registry,
fake_handler):
# Given
single_call['params'] = 'spam'
fake_method_registry.get_handler.return_value = fake_handler
mock_shared_registry.return_value = fake_method_registry
the_executor = executor.Executor()
# When
try:
_ = the_executor.get_call_spec(single_call)
except Exception as exception:
# Then
assert isinstance(exception, exceptions.JSONRPCInvalidParamsError)
else:
assert False, 'No exception raised?'
@mock.patch.object(executor.MethodRegistry, 'shared_registry')
def test_get_call_spec_with_args(mock_shared_registry,
single_call,
fake_method_registry,
fake_handler):
# Given
single_call['params'] = ['spam']
fake_method_registry.get_handler.return_value = fake_handler
mock_shared_registry.return_value = fake_method_registry
the_executor = executor.Executor()
with mock.patch.object(the_executor, 'enrich_args') as mock_enrich_args:
mock_enrich_args.return_value = ['spam', 'eggs']
# When
result = the_executor.get_call_spec(single_call)
# Then
assert result[2] == ['spam', 'eggs']
assert result[3] == {}
mock_enrich_args.assert_called_with(['spam'])
@mock.patch.object(executor.MethodRegistry, 'shared_registry')
def test_get_call_spec_with_kwargs(mock_shared_registry,
single_call,
fake_method_registry,
fake_handler):
# Given
single_call['params'] = {'spam': True}
fake_method_registry.get_handler.return_value = fake_handler
mock_shared_registry.return_value = fake_method_registry
the_executor = executor.Executor()
with mock.patch.object(the_executor, 'enrich_kwargs') as mock_enrich_kwargs:
mock_enrich_kwargs.return_value = {'spam': True, 'eggs': False}
# When
result = the_executor.get_call_spec(single_call)
# Then
assert result[2] == []
assert result[3] == {'spam': True, 'eggs': False}
mock_enrich_kwargs.assert_called_with({'spam': True})
def test_process_results(batch_calls, single_call, jsonrpc_error):
# Given
call_without_id = {**single_call}
call_without_id.pop('id')
call_results = [
(batch_calls[0], 'OK'),
(batch_calls[1], jsonrpc_error),
(call_without_id, '???'),
(call_without_id, jsonrpc_error),
]
the_executor = executor.Executor()
# When
result = the_executor.process_results(call_results)
# Then
assert isinstance(result, list)
assert len(result) == 2
first_response, second_response = result
expected_first_response = {
'jsonrpc': '2.0',
'id': batch_calls[0]['id'],
'result': 'OK',
}
assert first_response == expected_first_response
expected_second_response = {
'jsonrpc': '2.0',
'id': batch_calls[1]['id'],
'error': jsonrpc_error,
}
assert second_response == expected_second_response
def test_process_results_single_call(single_call):
# Given
call_results = [
(single_call, 'OK'),
]
the_executor = executor.Executor()
# When
result = the_executor.process_results(call_results)
# Then
expected_result = {
'jsonrpc': '2.0',
'id': single_call['id'],
'result': 'OK',
}
assert result == expected_result
def test_process_results_top_level_error(jsonrpc_error):
# Given
call_results = [
(None, jsonrpc_error),
]
the_executor = executor.Executor()
# When
result = the_executor.process_results(call_results)
# Then
expected_result = {
'jsonrpc': '2.0',
'id': None,
'error': jsonrpc_error,
}
assert result == expected_result
def test_process_results_empty(single_call):
# Given
single_call.pop('id')
call_results = [
(single_call, 'OK'),
]
the_executor = executor.Executor()
# When
result = the_executor.process_results(call_results)
# Then
assert result is None
def test_call_context_invalid_context(jsonrpc_error,
execute_context,
single_call):
# Given
the_executor = executor.Executor()
with mock.patch.object(the_executor, 'get_call_spec') as mock_get_call_spec:
mock_get_call_spec.side_effect = jsonrpc_error
# When
with the_executor.call_context(execute_context, single_call) as result:
pass
# Then
assert result.is_valid is False
def test_call_context_handle_jsonrpc_error(fake_handler,
jsonrpc_error,
execute_context,
single_call):
# Given
the_executor = executor.Executor()
with mock.patch.object(the_executor, 'get_call_spec') as mock_get_call_spec:
mock_get_call_spec.return_value = (
'test', fake_handler, [], {},
)
# When
with the_executor.call_context(execute_context, single_call) as _:
raise jsonrpc_error
# Then
assert len(execute_context.results) == 1
call_result = execute_context.results[0]
assert call_result[1] == jsonrpc_error
def test_call_context_handle_exception(fake_handler,
execute_context,
single_call):
# Given
the_executor = executor.Executor()
with mock.patch.object(the_executor, 'get_call_spec') as mock_get_call_spec:
mock_get_call_spec.return_value = (
'test', fake_handler, [], {},
)
# When
with the_executor.call_context(execute_context, single_call) as _:
raise RuntimeError('I HAZ FIAL')
# Then
assert len(execute_context.results) == 1
call_result = execute_context.results[0]
assert isinstance(call_result[1], exceptions.JSONRPCInternalError)
assert call_result[1].data == 'I HAZ FIAL'
def test_call_context(fake_handler, execute_context, single_call):
# Given
the_executor = executor.Executor()
with mock.patch.object(the_executor, 'get_call_spec') as mock_get_call_spec:
mock_get_call_spec.return_value = (
'test', fake_handler, [], {},
)
# When
with the_executor.call_context(execute_context, single_call) as result:
result.result = 'OK'
# Then
assert result.method == 'test'
assert result.handler is fake_handler
assert result.args == []
assert result.kwargs == {}
assert len(execute_context.results) == 1
expected_call_result = (single_call, 'OK')
assert execute_context.results[0] == expected_call_result
def test_execute_context_handle_jsonrpc_error(jsonrpc_error):
# Given
the_executor = executor.Executor()
# When
with the_executor.execute_context() as result:
raise jsonrpc_error
# Then
assert result.results == [(None, jsonrpc_error)]
def test_execute_context_handle_exception():
# Given
error = RuntimeError('I HAZ FIAL')
the_executor = executor.Executor()
# When
try:
with the_executor.execute_context() as result:
raise error
except Exception as exception:
assert exception is error
assert result.serializer is None
def test_execute_context_handle_empty_results(single_call):
# Given
the_executor = executor.Executor()
with mock.patch.object(the_executor, 'process_results') as mock_process_results:
with mock.patch.object(the_executor, 'serializer') as mock_serializer:
mock_process_results.return_value = None
# When
with the_executor.execute_context() as result:
result.results.append((single_call, 'OK'))
# Then
assert result.serializer is None
assert mock_serializer.called is False
def test_execute_context(fake_rpc_serializer, single_call):
# Given
fake_responses = {
'jsonrpc': '2.0',
'id': 'test',
'result': 'OK',
}
the_executor = executor.Executor()
with mock.patch.object(the_executor, 'process_results') as mock_process_results:
with mock.patch.object(the_executor, 'serializer') as mock_serializer:
mock_process_results.return_value = fake_responses
mock_serializer.return_value = fake_rpc_serializer
# When
with the_executor.execute_context() as result:
result.results.append((single_call, 'OK'))
# Then
assert result.serializer is fake_rpc_serializer
mock_process_results.assert_called_with([(single_call, 'OK')])
mock_serializer.assert_called_with(fake_responses)
def test_enrich_args():
# Given
the_executor = executor.Executor()
# When
result = the_executor.enrich_args(['spam', 'eggs'])
# Then
assert result == ['spam', 'eggs']
def test_enrich_kwargs():
# Given
the_executor = executor.Executor()
# When
result = the_executor.enrich_kwargs({'spam': True, 'eggs': False})
# Then
assert result == {'spam': True, 'eggs': False}
@pytest.mark.skip('NOOP')
def test_before_call():
pass
@mock.patch.object(executor.MethodRegistry, 'shared_registry')
def test_execute(mock_shared_registry, fake_method_registry):
# Given
fake_method_registry.get_handler.return_value = None
fake_method_registry.get_methods.return_value = []
mock_shared_registry.return_value = fake_method_registry
batch = [
{
'jsonrpc': '2.0',
'id': 'call_1',
'method': 'system.list_methods',
'params': ['spam'],
},
{
'jsonrpc': '2.0',
'id': 'call_2',
'method': 'idontexist',
},
{
'jsonrpc': '2.0',
'method': 'system.list_methods',
'params': {'spam': True},
},
]
payload = json.dumps(batch)
the_executor = executor.Executor()
with mock.patch.object(the_executor, 'before_call') as mock_before_call:
# When
result = the_executor.execute(payload)
# Then
assert isinstance(result, JSONRPCSerializer)
expected_result_data = [
{
'jsonrpc': '2.0',
'id': 'call_1',
'result': ['system.list_methods'],
},
{
'jsonrpc': '2.0',
'id': 'call_2',
'error': {
'code': exceptions.JSONRPCMethodNotFoundError.ERROR_CODE,
'message': exceptions.JSONRPCMethodNotFoundError.ERROR_MESSAGE,
},
},
]
assert result.data == expected_result_data
fake_method_registry.get_handler.assert_called_with(
'jsonrpc', 'idontexist',
)
assert mock_before_call.call_count == 2
mock_before_call.assert_any_call('system.list_methods', ['spam'], {})
mock_before_call.assert_any_call(
'system.list_methods', [], {'spam': True},
)

View File

@@ -0,0 +1,108 @@
# -*- coding: utf-8 -*-
from unittest import mock
from bthlabs_jsonrpc_core import registry
def test_init():
# When
result = registry.MethodRegistry()
# Then
assert result.registry == {'jsonrpc': {}}
@mock.patch.object(registry.MethodRegistry, '__init__')
def test_shared_registry(mock_init):
# Given
mock_init.return_value = None
# When
result = registry.MethodRegistry.shared_registry()
# Then
assert isinstance(result, registry.MethodRegistry)
assert registry.MethodRegistry.INSTANCE is result
assert mock_init.called is True
# After
registry.MethodRegistry.INSTANCE = None
@mock.patch.object(registry.MethodRegistry, '__init__')
def test_shared_registry_with_instance(mock_init, fake_method_registry):
# Given
mock_init.return_value = None
registry.MethodRegistry.INSTANCE = fake_method_registry
# When
result = registry.MethodRegistry.shared_registry()
# Then
assert result is fake_method_registry
assert registry.MethodRegistry.INSTANCE is fake_method_registry
assert mock_init.called is False
# After
registry.MethodRegistry.INSTANCE = None
def test_register_method(fake_handler):
# Given
the_registry = registry.MethodRegistry()
# When'
the_registry.register_method('testing', 'test', fake_handler)
# Then
expected_namespace = {'test': fake_handler}
assert the_registry.registry['testing'] == expected_namespace
def test_register_method_existing_namespace(fake_handler):
# Given
spam_handler = mock.Mock()
the_registry = registry.MethodRegistry()
the_registry.registry['jsonrpc']['spam'] = spam_handler
# When'
the_registry.register_method('jsonrpc', 'test', fake_handler)
# Then
expected_namespace = {'spam': spam_handler, 'test': fake_handler}
assert the_registry.registry['jsonrpc'] == expected_namespace
def test_get_methods():
# Given
the_registry = registry.MethodRegistry()
the_registry.registry['jsonrpc']['spam'] = mock.Mock()
the_registry.registry['jsonrpc']['eggs'] = mock.Mock()
# When'
result = the_registry.get_methods('jsonrpc')
# Then
expected_methods = {'spam', 'eggs'}
assert set(result) == expected_methods
def test_get_handler(fake_handler):
# Given
spam_handler = mock.Mock()
the_registry = registry.MethodRegistry()
the_registry.registry['jsonrpc']['spam'] = spam_handler
the_registry.registry['jsonrpc']['eggs'] = fake_handler
# When'
result = the_registry.get_handler('jsonrpc', 'eggs')
# Then
assert result is fake_handler

View File

@@ -0,0 +1,193 @@
# -*- coding: utf-8 -*-
import datetime
import decimal
import uuid
import pytest
from bthlabs_jsonrpc_core import exceptions, serializer
def test_init():
# When
result = serializer.JSONRPCSerializer('spam')
# Then
assert result._data == 'spam'
@pytest.mark.parametrize(
'value,expected',
[(None, True), (False, True), (0, True), ('spam', True), ([], False)],
)
def test_is_simple_value(value, expected):
# Given
the_serializer = serializer.JSONRPCSerializer('spam')
# When
result = the_serializer.is_simple_value(value)
# Then
assert result == expected
@pytest.mark.parametrize(
'value,expected',
[
([], True), ((x for x in [0, 1]), True), (set(), True),
(tuple(), True), ({}, False),
],
)
def test_is_sequence_value(value, expected):
# Given
the_serializer = serializer.JSONRPCSerializer('spam')
# When
result = the_serializer.is_sequence_value(value)
# Then
assert result == expected
@pytest.mark.parametrize(
'value,expected',
[({}, True), ([], False)],
)
def test_is_dict_value(value, expected):
# Given
the_serializer = serializer.JSONRPCSerializer('spam')
# When
result = the_serializer.is_dict_value(value)
# Then
assert result == expected
@pytest.mark.parametrize(
'value,expected',
[(uuid.uuid4(), True), (decimal.Decimal('42'), True), ([], False)],
)
def test_is_string_coercible_value(value, expected):
# Given
the_serializer = serializer.JSONRPCSerializer('spam')
# When
result = the_serializer.is_string_coercible_value(value)
# Then
assert result == expected
@pytest.mark.parametrize(
'value,expected',
[
(
datetime.datetime(1987, 10, 3, 8, 0, 0, 0, tzinfo=datetime.timezone.utc),
'1987-10-03T08:00:00+00:00',
),
(
datetime.date(1987, 10, 3),
'1987-10-03',
),
],
)
def test_serialize_datetime(value, expected):
# Given
the_serializer = serializer.JSONRPCSerializer('spam')
# When
result = the_serializer.serialize_datetime(value)
# Then
assert result == expected
def test_serialize_sequence():
# Given
the_serializer = serializer.JSONRPCSerializer('spam')
# When
result = the_serializer.serialize_sequence([(0, 1), 'spam'])
# Then
assert result == [[0, 1], 'spam']
def test_serialize_dict():
# Given
the_serializer = serializer.JSONRPCSerializer('spam')
# When
result = the_serializer.serialize_dict(
{'spam': True, 'eggs': {'key': decimal.Decimal('42')}},
)
# Then
assert result == {'spam': True, 'eggs': {'key': '42'}}
def test_serialize_string_coercible():
# Given
the_serializer = serializer.JSONRPCSerializer('spam')
# When
result = the_serializer.serialize_string_coercible(decimal.Decimal('42'))
# Then
assert result == '42'
def test_serialize_value():
# Given
value = [
serializer.JSONRPCSerializer('spam'),
'eggs',
datetime.datetime(1987, 10, 3, 8, 0, 0, 0, tzinfo=datetime.timezone.utc),
[0, 1],
{'spam': True},
decimal.Decimal('42'),
exceptions.BaseJSONRPCError(),
]
the_serializer = serializer.JSONRPCSerializer('spam')
# When
result = the_serializer.serialize_value(value)
# Then
expected = [
'spam',
'eggs',
'1987-10-03T08:00:00+00:00',
[0, 1],
{'spam': True},
'42',
exceptions.BaseJSONRPCError().to_rpc(),
]
assert result == expected
def test_serialize_value_no_to_rpc():
# Given
the_serializer = serializer.JSONRPCSerializer('spam')
# When
try:
_ = the_serializer.serialize_value(object())
except Exception as exception:
# Then
assert isinstance(exception, exceptions.JSONRPCSerializerError)
def test_data():
# Given
the_serializer = serializer.JSONRPCSerializer(decimal.Decimal('42'))
# When
result = the_serializer.data
# Then
assert result == '42'
assert the_serializer._serialized_data == '42'