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:
Tomek Wójcik 2022-06-04 10:41:53 +02:00
commit c75ea4ea9d
111 changed files with 7193 additions and 0 deletions

14
.gitignore vendored Normal file
View File

@ -0,0 +1,14 @@
*.pyc
*.pyo
*.swp
.envrc
.venv
invoke.json
tasks.py
.mypy_cache/
.pytest_cache/
build/
dist/
ops/

12
README.md Normal file
View File

@ -0,0 +1,12 @@
# bthlabs-jsonrpc
BTHLabs JSONRPC is a set of Python libraries that provide extensible framework
for adding JSONRPC interfaces to existing Python Web applications.
## Author
*bthlabs-jsonrpc* is developed by [Tomek Wójcik](https://www.bthlabs.pl/).
## License
*bthlabs-jsonrpc-core* is licensed under the MIT License.

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,55 @@
bthlabs-jsonrpc-aiohttp
=======================
BTHLabs JSONRPC - aiohttp integration
`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 *aiohttp* package provides aiohttp integration.
Installation
------------
.. code-block:: shell
$ pip install bthlabs_jsonrpc_aiohttp
Example
-------
.. code-block:: python
# app.py
from aiohttp import web
from bthlabs_jsonrpc_core import register_method
from bthlabs_jsonrpc_aiohttp import JSONRPCView
@register_method('hello')
async def hello(request, who='World'):
return f'Hello, {who}!'
app = web.Application()
app.add_routes([
web.post('/rpc', JSONRPCView()),
])
Author
------
*bthlabs-jsonrpc-aiohttp* is developed by `Tomek Wójcik`_.
License
-------
*bthlabs-jsonrpc-aiohttp* is licensed under the MIT License.
.. _Docs: https://projects.bthlabs.pl/bthlabs-jsonrpc/aiohttp/
.. _Source repository: https://git.bthlabs.pl/tomekwojcik/bthlabs-jsonrpc/
.. _Tomek Wójcik: https://www.bthlabs.pl/

View File

@ -0,0 +1,5 @@
# -*- coding: utf-8 -*-
# django-jsonrpc-aiohttp | (c) 2022-present Tomek Wójcik | MIT License
from .views import JSONRPCView # noqa
__version__ = '1.0.0'

View File

@ -0,0 +1,53 @@
# -*- coding: utf-8 -*-
# django-jsonrpc-aiohttp | (c) 2022-present Tomek Wójcik | MIT License
import logging
from bthlabs_jsonrpc_core import Executor, JSONRPCAccessDeniedError
from bthlabs_jsonrpc_core.exceptions import JSONRPCParseError
LOGGER = logging.getLogger('bthlabs_jsonrpc.aiohttp.executor')
class AioHttpExecutor(Executor):
def __init__(self, request, can_call, namespace=None):
super().__init__(namespace=namespace)
self.request = request
self.can_call = can_call
async def list_methods(self, *args, **kwargs):
return super().list_methods()
async def deserialize_data(self, request):
try:
return await request.json()
except Exception as exception:
LOGGER.error('Error deserializing RPC call!', exc_info=exception)
raise JSONRPCParseError()
def enrich_args(self, args):
return [self.request, *super().enrich_args(args)]
async def before_call(self, method, args, kwargs):
can_call = await self.can_call(self.request, method, args, kwargs)
if can_call is False:
raise JSONRPCAccessDeniedError(data='can_call')
async def execute(self):
with self.execute_context() as execute_context:
data = await self.deserialize_data(self.request)
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:
await self.before_call(
call_context.method,
call_context.args,
call_context.kwargs,
)
call_context.result = await call_context.handler(
*call_context.args, **call_context.kwargs,
)
return execute_context.serializer

View File

@ -0,0 +1,53 @@
# -*- coding: utf-8 -*-
# django-jsonrpc-aiohttp | (c) 2022-present Tomek Wójcik | MIT License
import typing
from aiohttp import web
from bthlabs_jsonrpc_aiohttp.executor import AioHttpExecutor
class JSONRPCView:
"""
The JSONRPC View. This is the main JSONRPC entry point. Use it to register
your JSONRPC endpoints.
Example:
.. code-block:: python
from bthlabs_jsonrpc_aiohttp import JSONRPCView
app.add_routes([
web.post('/rpc', JSONRPCView()),
web.post('/example/rpc', JSONRPCView(namespace='examnple')),
])
"""
# pragma mark - Public interface
def __init__(self, namespace: typing.Optional[str] = None):
self.namespace: typing.Optional[str] = namespace
async def can_call(self,
request: web.Request,
method: str,
args: list,
kwargs: dict) -> bool:
"""
Hook for subclasses to perform additional per-call permissions checks
etc. The default implementation returns ``True``.
"""
return True
async def __call__(self, request: web.Request) -> web.Response:
"""The request handler."""
executor = AioHttpExecutor(
request, self.can_call, namespace=self.namespace,
)
serializer = await executor.execute()
if serializer is None:
return web.Response(body='')
return web.json_response(serializer.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,13 @@
API Documentation
=================
.. module:: bthlabs_jsonrpc_aiohttp
This section provides the API documentation for BTHLabs JSONRPC - aiohttp.
Views
-----
.. autoclass:: JSONRPCView
:members:
:special-members: __call__

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 - aiohttp'
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,17 @@
BTHLabs JSONRPC - aiohttp
=========================
BTHLabs JSONRPC is a set of Python libraries that provide extensible framework
for adding JSONRPC interfaces to existing Python Web applications.
The *aiohttp* package provides aiohttp integration.
.. toctree::
:maxdepth: 2
overview
.. toctree::
:maxdepth: 2
api

View File

@ -0,0 +1,36 @@
Overview
========
This section provides the general overview of the integration.
Installation
------------
.. code-block:: shell
$ pip install bthlabs_jsonrpc_aiohttp
Usage
-----
First, you'll need to add a JSONRPC view to your project's URLs:
.. code-block:: python
# app.py
app = web.Application()
app.add_routes([
web.post('/rpc', JSONRPCView()),
])
Then, you'll need to implement the RPC method modules:
.. code-block:: python
# your_app/rpc_methods.py
from bthlabs_jsonrpc_core import register_method
@register_method(name='hello')
async def hello(request, who='World'):
return f'Hello, {who}!'

View File

@ -0,0 +1,63 @@
# -*- coding: utf-8 -*-
# django-jsonrpc-aiohttp | (c) 2022-present Tomek Wójcik | MIT License
import asyncio
import logging
import os
import sys
from aiohttp import web
from bthlabs_jsonrpc_core import register_method
from bthlabs_jsonrpc_aiohttp import JSONRPCView
logger = logging.getLogger('bthlabs_jsonrpc_aiohttp_example')
logger.setLevel(logging.DEBUG)
handler = logging.StreamHandler(sys.stdout)
formatter = logging.Formatter(
'%(asctime)s %(name)s: %(levelname)s: %(message)s',
)
handler.setFormatter(formatter)
logger.addHandler(handler)
app_logger = logging.getLogger('bthlabs_jsonrpc_aiohttp_example.app')
jsonrpc_logger = logger = logging.getLogger('bthlabs_jsonrpc')
jsonrpc_logger.setLevel(logging.DEBUG)
jsonrpc_logger.addHandler(handler)
async def app_on_startup(app):
logger.info('BTHLabs JSONRPC aiohttp integration example')
logger.debug('My PID = {pid}'.format(pid=os.getpid()))
@register_method('hello')
async def hello(request):
return 'Hello, World!'
@register_method('async_test')
async def async_test(request, delay):
await asyncio.sleep(delay)
return 'It works!'
@register_method('hello', namespace='example')
async def hello_example(request):
return 'Hello, Example!'
def create_app(loop=None):
app = web.Application(logger=app_logger, loop=loop)
app.on_startup.append(app_on_startup)
app.add_routes([
web.post('/rpc', JSONRPCView()),
web.post('/example/rpc', JSONRPCView(namespace='example')),
])
return app
app = create_app()

View File

@ -0,0 +1,3 @@
#!/bin/bash
# django-jsonrpc-aiohttp | (c) 2022-present Tomek Wójcik | MIT License
exec adev runserver example.py

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -0,0 +1,32 @@
[tool.poetry]
name = "bthlabs-jsonrpc-aiohttp"
version = "1.0.0"
description = "BTHLabs JSONRPC - aiohttp integration"
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/aiohttp/"
[tool.poetry.dependencies]
python = "^3.10"
aiohttp = ">=3.6,<4.0"
bthlabs-jsonrpc-core = "1.0.0"
[tool.poetry.dev-dependencies]
bthlabs-jsonrpc-core = { path = "../bthlabs-jsonrpc-core", develop = true }
aiohttp-devtools = "1.0.post0"
flake8 = "4.0.1"
flake8-commas = "2.1.0"
mypy = "0.950"
pytest = "7.1.2"
pytest-aiohttp = "1.0.4"
pytest-asyncio = "0.18.3"
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,7 @@
[flake8]
exclude = .venv/,.pytest_cache/
ignore = E402
max-line-length = 119
[tool:pytest]
asyncio_mode = auto

View File

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

View File

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
from unittest import mock
from aiohttp.web import Request
import pytest
@pytest.fixture
def fake_request():
return mock.Mock(spec=Request)

View File

@ -0,0 +1,170 @@
# -*- coding: utf-8 -*-
from unittest import mock
from bthlabs_jsonrpc_core import exceptions, serializer
import pytest
from bthlabs_jsonrpc_aiohttp import executor
@pytest.fixture
def fake_can_call():
result = mock.AsyncMock()
result.return_value = True
return result
def test_init(fake_request, fake_can_call):
# When
result = executor.AioHttpExecutor(fake_request, fake_can_call)
# Then
assert result.request == fake_request
assert result.can_call == fake_can_call
async def test_list_methods(fake_request, fake_can_call):
# Given
the_executor = executor.AioHttpExecutor(fake_request, fake_can_call)
# When
result = await the_executor.list_methods()
# Then
assert result == ['system.list_methods']
async def test_deserialize_data(fake_request, fake_can_call):
# Given
fake_request.json.return_value = 'spam'
the_executor = executor.AioHttpExecutor(fake_request, fake_can_call)
# When
result = await the_executor.deserialize_data(fake_request)
# Then
assert result == 'spam'
async def test_deserialize_data_error(fake_request, fake_can_call):
# Given
fake_request.json.side_effect = RuntimeError('I HAZ FAIL')
the_executor = executor.AioHttpExecutor(fake_request, fake_can_call)
# When
try:
_ = await the_executor.deserialize_data(fake_request)
except Exception as exception:
assert isinstance(exception, exceptions.JSONRPCParseError)
else:
assert False, 'No exception raised?'
def test_enrich_args(fake_request, fake_can_call):
# Given
the_executor = executor.AioHttpExecutor(fake_request, fake_can_call)
# When
result = the_executor.enrich_args(['spam'])
# Then
assert result == [fake_request, 'spam']
async def test_before_call(fake_request, fake_can_call):
# Given
the_executor = executor.AioHttpExecutor(fake_request, fake_can_call)
# When
await the_executor.before_call('test', ['spam'], {'spam': True})
# Then
fake_can_call.assert_called_with(
fake_request, 'test', ['spam'], {'spam': True},
)
async def test_before_call_access_denied(fake_request, fake_can_call):
# Given
fake_can_call.return_value = False
the_executor = executor.AioHttpExecutor(fake_request, fake_can_call)
# When
try:
await the_executor.before_call('test', ['spam'], {'spam': True})
except Exception as exception:
assert isinstance(exception, exceptions.JSONRPCAccessDeniedError)
else:
assert False, 'No exception raised?'
@mock.patch('bthlabs_jsonrpc_core.registry.MethodRegistry.shared_registry')
async def test_execute(mock_shared_registry, fake_request, fake_can_call):
# Given
fake_method_registry = mock.Mock()
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},
},
]
fake_request.json.return_value = batch
the_executor = executor.AioHttpExecutor(fake_request, fake_can_call)
with mock.patch.object(the_executor, 'before_call') as mock_before_call:
# When
result = await the_executor.execute()
# Then
assert isinstance(result, serializer.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', [fake_request, 'spam'], {},
)
mock_before_call.assert_any_call(
'system.list_methods', [fake_request], {'spam': True},
)

View File

@ -0,0 +1,148 @@
# -*- coding: utf-8 -*-
from unittest import mock
from aiohttp import web
from bthlabs_jsonrpc_core import exceptions
from bthlabs_jsonrpc_aiohttp import views
def test_init():
# When
result = views.JSONRPCView()
# Then
assert result.namespace is None
def test_init_with_namespace():
# When
result = views.JSONRPCView(namespace='testing')
# Then
assert result.namespace == 'testing'
async def test_can_call(fake_request):
# Given
view = views.JSONRPCView()
# When
result = await view.can_call(fake_request, 'test', [], {})
# Then
assert result is True
async def test_view(aiohttp_client):
# Given
view = views.JSONRPCView()
app = web.Application()
app.router.add_post('/', view)
client = await aiohttp_client(app)
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},
},
]
# When
response = await client.post('/', json=batch)
# Then
assert response.status == 200
data = await response.json()
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 data == expected_result_data
async def test_view_empty_response(aiohttp_client):
# Given
view = views.JSONRPCView()
app = web.Application()
app.router.add_post('/', view)
client = await aiohttp_client(app)
call = {
'jsonrpc': '2.0',
'method': 'system.list_methods',
}
# When
response = await client.post('/', json=call)
# Then
assert response.status == 200
data = await response.content.read()
assert data == b''
async def test_view_permission_denied(aiohttp_client):
# Given
view = views.JSONRPCView()
app = web.Application()
app.router.add_post('/', view)
client = await aiohttp_client(app)
call = {
'jsonrpc': '2.0',
'id': 'call_1',
'method': 'system.list_methods',
}
with mock.patch.object(view, 'can_call') as mock_can_call:
mock_can_call.return_value = False
# When
response = await client.post('/', json=call)
# Then
assert response.status == 200
data = await response.json()
expected_result_data = {
'jsonrpc': '2.0',
'id': 'call_1',
'error': {
'code': exceptions.JSONRPCAccessDeniedError.ERROR_CODE,
'message': exceptions.JSONRPCAccessDeniedError.ERROR_MESSAGE,
'data': 'can_call',
},
}
assert data == expected_result_data

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'

View File

@ -0,0 +1,19 @@
Copyright (c) 2023-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,71 @@
bthlabs-jsonrpc-django
======================
BTHLabs JSONRPC - django integration
`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 *django* package provides Django integration.
Installation
------------
.. code-block:: shell
$ pip install bthlabs_jsonrpc_django
Example
-------
.. code-block:: python
# settings.py
INSTALLED_APPS = [
# ...
'bthlabs_jsonrpc_django',
]
.. code-block:: python
# settings.py
JSONRPC_METHOD_MODULES = [
# ...
'your_app.rpc_methods',
]
.. code-block:: python
# urls.py
urlpatterns = [
# ...
path('rpc', JSONRPCView.as_view()),
]
.. code-block:: python
# your_app/rpc_methods.py
from bthlabs_jsonrpc_core import register_method
@register_method(name='hello')
def hello(request, who='World'):
return f'Hello, {who}!'
Author
------
*bthlabs-jsonrpc-django* is developed by `Tomek Wójcik`_.
License
-------
*bthlabs-jsonrpc-django* is licensed under the MIT License.
.. _Docs: https://projects.bthlabs.pl/bthlabs-jsonrpc/django/
.. _Source repository: https://git.bthlabs.pl/tomekwojcik/bthlabs-jsonrpc/
.. _Tomek Wójcik: https://www.bthlabs.pl/

View File

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# django-jsonrpc-django | (c) 2022-present Tomek Wójcik | MIT License
from .auth_checks import ( # noqa
has_perms,
is_authenticated,
is_staff,
)
from .views import JSONRPCView # noqa
__version__ = '1.0.0'

View File

@ -0,0 +1,17 @@
# -*- coding: utf-8 -*-
# django-jsonrpc-django | (c) 2022-present Tomek Wójcik | MIT License
import importlib
from django.apps import AppConfig
class BTHLabsJSONRPCConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'bthlabs_jsonrpc_django'
verbose_name = 'BTHLabs JSONRPC'
def ready(self):
from django.conf import settings
for module_path in settings.JSONRPC_METHOD_MODULES:
_ = importlib.import_module(module_path)

View File

@ -0,0 +1,27 @@
# -*- coding: utf-8 -*-
# django-jsonrpc-django | (c) 2022-present Tomek Wójcik | MIT License
import typing
from django.http import HttpRequest
def is_authenticated(request: HttpRequest) -> bool:
"""Checks if the request user is authenticated and active."""
return all((
request.user.is_anonymous is False,
request.user.is_active is True,
))
def is_staff(request: HttpRequest) -> bool:
"""Checks if the request user is a staff user."""
return request.user.is_staff
def has_perms(perms: list[str]) -> typing.Callable:
"""Checks if the request user has the specified permissions."""
def internal_has_perms(request: HttpRequest) -> bool:
return request.user.has_perms(perms)
return internal_has_perms

View File

@ -0,0 +1,28 @@
# -*- coding: utf-8 -*-
# django-jsonrpc-django | (c) 2022-present Tomek Wójcik | MIT License
import typing
from bthlabs_jsonrpc_core import Executor, JSONRPCAccessDeniedError
from django.http import HttpRequest
from bthlabs_jsonrpc_django.serializer import DjangoJSONRPCSerializer
class DjangoExecutor(Executor):
serializer = DjangoJSONRPCSerializer
def __init__(self,
request: HttpRequest,
can_call: typing.Callable,
namespace: typing.Optional[str] = None):
super().__init__(namespace=namespace)
self.request: HttpRequest = request
self.can_call: typing.Callable = can_call
def enrich_args(self, args):
return [self.request, *super().enrich_args(args)]
def before_call(self, method, args, kwargs):
can_call = self.can_call(self.request, method, args, kwargs)
if can_call is False:
raise JSONRPCAccessDeniedError(data='can_call')

View File

@ -0,0 +1,8 @@
# -*- coding: utf-8 -*-
# django-jsonrpc-django | (c) 2022-present Tomek Wójcik | MIT License
from bthlabs_jsonrpc_core import JSONRPCSerializer
from django.db.models import QuerySet
class DjangoJSONRPCSerializer(JSONRPCSerializer):
SEQUENCE_TYPES = (QuerySet, *JSONRPCSerializer.SEQUENCE_TYPES)

View File

@ -0,0 +1,122 @@
# -*- coding: utf-8 -*-
# django-jsonrpc-django | (c) 2022-present Tomek Wójcik | MIT License
import typing
from bthlabs_jsonrpc_core import Executor
from django.core.exceptions import PermissionDenied
from django.http import HttpRequest, HttpResponse, JsonResponse
from django.utils.decorators import classonlymethod
from django.views.decorators.csrf import csrf_exempt
from django.views.generic.base import View
from bthlabs_jsonrpc_django.executor import DjangoExecutor
class JSONRPCView(View):
"""
The JSONRPC View. This is the main JSONRPC entry point. Use it to register
your JSONRPC endpoints.
Example:
.. code-block:: python
from bthlabs_jsonrpc_django import JSONRPCView, is_authenticated
urlpatterns = [
path(
'rpc/private',
JSONRPCView.as_view(
auth_checks=[is_authenticated],
namespace='admin',
),
)
path('rpc', JSONRPCView.as_view()),
]
"""
# pragma mark - Private class attributes
# The executor class.
executor: Executor = DjangoExecutor
# pragma mark - Public class attributes
#: List of auth check functions.
auth_checks: list[typing.Callable] = []
#: Namespace of this endpoint.
namespace: typing.Optional[str] = None
# pragma mark - Private interface
def ensure_auth(self, request: HttpRequest) -> None:
"""
Runs auth checks (if any) and raises
:py:exc:`django.core.exceptions.PermissionDenied` if any of them
returns ``False``.
:meta private:
"""
if len(self.auth_checks) == []:
return
has_auth = all((
auth_check(request)
for auth_check
in self.auth_checks
))
if has_auth is False:
raise PermissionDenied('This RPC endpoint requires auth.')
def dispatch(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
"""
Dispatches the *request*.
:meta private:
"""
if request.method.lower() in self.http_method_names:
handler = getattr(
self, request.method.lower(), self.http_method_not_allowed,
)
else:
handler = self.http_method_not_allowed
self.ensure_auth(request)
return handler(request, *args, **kwargs)
def post(self, request: HttpRequest) -> HttpResponse:
"""
The POST handler.
:meta private:
"""
executor = self.executor(
request, self.can_call, self.namespace,
)
serializer = executor.execute(request.body)
if serializer is None:
return HttpResponse('')
return JsonResponse(serializer.data, safe=False)
# pragma mark - Public interface
@classonlymethod
def as_view(cls, **initkwargs):
result = super().as_view(**initkwargs)
return csrf_exempt(result)
def can_call(self,
request: HttpRequest,
method: str,
args: list,
kwargs: dict) -> bool:
"""
Hook for subclasses to perform additional per-call permissions checks
etc. The default implementation returns ``True``.
"""
return True

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,21 @@
API Documentation
=================
.. module:: bthlabs_jsonrpc_django
This section provides the API documentation for BTHLabs JSONRPC - Core.
Auth checks
-----------
.. autofunction:: has_perms
.. autofunction:: is_authenticated
.. autofunction:: is_staff
Views
-----
.. autoclass:: JSONRPCView
:members: as_view, auth_checks, can_call, namespace

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 - Django'
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,17 @@
BTHLabs JSONRPC - Django
========================
BTHLabs JSONRPC is a set of Python libraries that provide extensible framework
for adding JSONRPC interfaces to existing Python Web applications.
The *django* package provides Django integration.
.. toctree::
:maxdepth: 2
overview
.. toctree::
:maxdepth: 2
api

View File

@ -0,0 +1,57 @@
Overview
========
This section provides the general overview of the integration.
Installation
------------
.. code-block:: shell
$ pip install bthlabs_jsonrpc_django
Usage
-----
First, you'll need to enable the application by adding it to
``INSTALLED_APPS``:
.. code-block:: python
# settings.py
INSTALLED_APPS = [
# ...
'bthlabs_jsonrpc_django',
]
Then, you'll need to add your RPC method modules to ``JSONRPC_METHOD_MODULES``
setting:
.. code-block:: python
# settings.py
JSONRPC_METHOD_MODULES = [
# ...
'your_app.rpc_methods',
]
After that, you'll need to add a JSONRPC view to your project's URLs:
.. code-block:: python
# urls.py
urlpatterns = [
# ...
path('rpc', JSONRPCView.as_view()),
]
Last but not least, you'll need to implement the RPC method modules:
.. code-block:: python
# your_app/rpc_methods.py
from bthlabs_jsonrpc_core import register_method
@register_method(name='hello')
def hello(request, who='World'):
return f'Hello, {who}!'

View File

@ -0,0 +1 @@
/db.sqlite3

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
"""
ASGI config for django_jsonrpc_django_example project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/4.0/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault(
'DJANGO_SETTINGS_MODULE',
'bthlabs_jsonrpc_django_example.settings.production',
)
application = get_asgi_application()

View File

@ -0,0 +1,6 @@
# -*- coding: utf-8 -*-
from django.contrib.auth.middleware import RemoteUserMiddleware
class CustomHeaderRemoteUserMiddleware(RemoteUserMiddleware):
header = 'HTTP_X_USER'

View File

@ -0,0 +1,76 @@
# -*- coding: utf-8 -*-
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent.parent
SECRET_KEY = None
DEBUG = False
ALLOWED_HOSTS = []
INSTALLED_APPS = [
# Django apps
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# 3rd party apps
'bthlabs_jsonrpc_django',
# Project apps
'bthlabs_jsonrpc_django_example.things',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'bthlabs_jsonrpc_django_example.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'bthlabs_jsonrpc_django_example.wsgi.application'
DATABASES = {
}
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
]
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_TZ = True
STATIC_URL = 'static/'
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
JSONRPC_METHOD_MODULES = [
'bthlabs_jsonrpc_django_example.things.rpc_methods',
]

View File

@ -0,0 +1,13 @@
# -*- coding: utf-8 -*-
# django-jsonrpc-django | (c) 2022-present Tomek Wójcik | MIT License
from django.contrib import admin
from bthlabs_jsonrpc_django_example.things import models
class ThingAdmin(admin.ModelAdmin):
list_display = ('pk', 'name', 'created_at', 'owner', 'is_active')
admin.site.register(models.Thing, ThingAdmin)

View File

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# django-jsonrpc-django | (c) 2022-present Tomek Wójcik | MIT License
from django.apps import AppConfig
class ThingsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'bthlabs_jsonrpc_django_example.things'
label = 'things'
verbose_name = 'Things'

View File

@ -0,0 +1,33 @@
# Generated by Django 4.0.4 on 2022-05-12 06:15
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Thing',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255)),
('content', models.TextField()),
('created_at', models.DateTimeField(auto_now_add=True)),
('modified_at', models.DateTimeField(auto_now=True)),
('is_active', models.BooleanField(db_index=True, default=True)),
('owner', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'thing',
'verbose_name_plural': 'things',
},
),
]

View File

@ -0,0 +1,30 @@
# -*- coding: utf-8 -*-
# django-jsonrpc-django | (c) 2022-present Tomek Wójcik | MIT License
from django.db import models
class Thing(models.Model):
name = models.CharField(max_length=255)
content = models.TextField()
created_at = models.DateTimeField(auto_now=False, auto_now_add=True)
modified_at = models.DateTimeField(auto_now=True, auto_now_add=False)
is_active = models.BooleanField(default=True, db_index=True)
owner = models.ForeignKey(
'auth.User', null=True, blank=True, on_delete=models.CASCADE,
)
class Meta:
verbose_name = 'thing'
verbose_name_plural = 'things'
def to_rpc(self):
return {
'id': self.pk,
'name': self.name,
'content': self.content,
'created_at': self.created_at,
'modified_at': self.modified_at,
'is_active': self.is_active,
'owner_id': self.owner_id,
}

View File

@ -0,0 +1,89 @@
# -*- coding: utf-8 -*-
# django-jsonrpc-django | (c) 2022-present Tomek Wójcik | MIT License
from bthlabs_jsonrpc_core import JSONRPCAccessDeniedError, register_method
from bthlabs_jsonrpc_django_example.things.models import Thing
@register_method('things.list')
def list_things(request):
return Thing.objects.filter(
is_active=True,
owner__isnull=True,
)
@register_method('things.list', namespace='private')
def private_list_things(request):
return Thing.objects.filter(
is_active=True,
owner=request.user,
)
@register_method('things.create', namespace='private')
def private_create_thing(request, name, content):
return Thing.objects.create(
name=name,
content=content,
is_active=True,
owner=request.user,
)
@register_method('things.update', namespace='private')
def private_update_thing(request, thing_id, name=None, content=None):
thing = Thing.objects.get(pk=thing_id, is_active=True)
if thing.owner != request.user:
raise JSONRPCAccessDeniedError("You can't access this thing.")
if name is not None:
thing.name = name
if content is not None:
thing.content = content
thing.save()
return thing
@register_method('things.list', namespace='admin')
def admin_list_things(request):
return Thing.objects.all()
@register_method('things.create', namespace='admin')
def admin_create_thing(request, name, content, is_active, owner_id):
return Thing.objects.create(
name=name,
content=content,
is_active=is_active,
owner_id=owner_id,
)
@register_method('things.update', namespace='admin')
def admin_update_thing(request,
thing_id,
name=None,
content=None,
is_active=None,
owner_id=None):
thing = Thing.objects.get(pk=thing_id)
if name is not None:
thing.name = name
if content is not None:
thing.content = content
if is_active is not None:
thing.is_active = is_active
if owner_id is not None:
thing.owner_id = owner_id
thing.save()
return thing

View File

@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
from django.contrib import admin
from django.urls import path
from bthlabs_jsonrpc_django import JSONRPCView, is_authenticated, is_staff
urlpatterns = [
path('admin/', admin.site.urls),
path(
'rpc/admin',
JSONRPCView.as_view(
auth_checks=[is_authenticated, is_staff],
namespace='admin',
),
),
path(
'rpc/private',
JSONRPCView.as_view(
auth_checks=[is_authenticated],
namespace='private',
),
),
path('rpc', JSONRPCView.as_view()),
]

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
"""
WSGI config for django_jsonrpc_django_example project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/4.0/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault(
'DJANGO_SETTINGS_MODULE',
'bthlabs_jsonrpc_django_example.settings.production',
)
application = get_wsgi_application()

View File

@ -0,0 +1,27 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
"""Run administrative tasks."""
os.environ.setdefault(
'DJANGO_SETTINGS_MODULE',
'bthlabs_jsonrpc_django_example.settings.local',
)
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
),
) from exc
execute_from_command_line(sys.argv)
if __name__ == '__main__':
main()

View File

@ -0,0 +1,814 @@
[[package]]
name = "alabaster"
version = "0.7.12"
description = "A configurable sidebar-enabled Sphinx theme"
category = "dev"
optional = false
python-versions = "*"
[[package]]
name = "asgiref"
version = "3.5.2"
description = "ASGI specs, helper code, and adapters"
category = "main"
optional = false
python-versions = ">=3.7"
[package.extras]
tests = ["pytest", "pytest-asyncio", "mypy (>=0.800)"]
[[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 = "bthlabs-jsonrpc-core"
version = "1.0.0"
description = "BTHLabs JSONRPC - Core"
category = "main"
optional = false
python-versions = "^3.10"
develop = true
[package.source]
type = "directory"
url = "../bthlabs-jsonrpc-core"
[[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 = "django"
version = "3.2.13"
description = "A high-level Python Web framework that encourages rapid development and clean, pragmatic design."
category = "main"
optional = false
python-versions = ">=3.6"
[package.dependencies]
asgiref = ">=3.3.2,<4"
pytz = "*"
sqlparse = ">=0.2.2"
[package.extras]
argon2 = ["argon2-cffi (>=19.1.0)"]
bcrypt = ["bcrypt"]
[[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 = "factory-boy"
version = "3.2.1"
description = "A versatile test fixtures replacement based on thoughtbot's factory_bot for Ruby."
category = "dev"
optional = false
python-versions = ">=3.6"
[package.dependencies]
Faker = ">=0.7.0"
[package.extras]
dev = ["coverage", "django", "flake8", "isort", "pillow", "sqlalchemy", "mongoengine", "wheel (>=0.32.0)", "tox", "zest.releaser"]
doc = ["sphinx", "sphinx-rtd-theme", "sphinxcontrib-spelling"]
[[package]]
name = "faker"
version = "13.12.0"
description = "Faker is a Python package that generates fake data for you."
category = "dev"
optional = false
python-versions = ">=3.6"
[package.dependencies]
python-dateutil = ">=2.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 = "pytest-django"
version = "4.5.2"
description = "A Django plugin for pytest."
category = "dev"
optional = false
python-versions = ">=3.5"
[package.dependencies]
pytest = ">=5.4.0"
[package.extras]
docs = ["sphinx", "sphinx-rtd-theme"]
testing = ["django", "django-configurations (>=2.0)"]
[[package]]
name = "python-dateutil"
version = "2.8.2"
description = "Extensions to the standard Python datetime module"
category = "dev"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
[package.dependencies]
six = ">=1.5"
[[package]]
name = "pytz"
version = "2022.1"
description = "World timezone definitions, modern and historical"
category = "main"
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 = "six"
version = "1.16.0"
description = "Python 2 and 3 compatibility utilities"
category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
[[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 = "sqlparse"
version = "0.4.2"
description = "A non-validating SQL parser."
category = "main"
optional = false
python-versions = ">=3.5"
[[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 = "0d2fc52b8eaf0c5363f865d3c32b6a3f804e6429c798c5841d2ec45f5d56d222"
[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"},
]
asgiref = [
{file = "asgiref-3.5.2-py3-none-any.whl", hash = "sha256:1d2880b792ae8757289136f1db2b7b99100ce959b2aa57fd69dab783d05afac4"},
{file = "asgiref-3.5.2.tar.gz", hash = "sha256:4a29362a6acebe09bf1d6640db38c1dc3d9217c68e6f9f6204d72667fc19a424"},
]
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"},
]
bthlabs-jsonrpc-core = []
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"},
]
django = [
{file = "Django-3.2.13-py3-none-any.whl", hash = "sha256:b896ca61edc079eb6bbaa15cf6071eb69d6aac08cce5211583cfb41515644fdf"},
{file = "Django-3.2.13.tar.gz", hash = "sha256:6d93497a0a9bf6ba0e0b1a29cccdc40efbfc76297255b1309b3a884a688ec4b6"},
]
docutils = [
{file = "docutils-0.17.1-py2.py3-none-any.whl", hash = "sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61"},
{file = "docutils-0.17.1.tar.gz", hash = "sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125"},
]
factory-boy = [
{file = "factory_boy-3.2.1-py2.py3-none-any.whl", hash = "sha256:eb02a7dd1b577ef606b75a253b9818e6f9eaf996d94449c9d5ebb124f90dc795"},
{file = "factory_boy-3.2.1.tar.gz", hash = "sha256:a98d277b0c047c75eb6e4ab8508a7f81fb03d2cb21986f627913546ef7a2a55e"},
]
faker = [
{file = "Faker-13.12.0-py3-none-any.whl", hash = "sha256:5cbb89fc6a16793b2bd98252c03a86098c7426beab0a20382709a815651b8804"},
{file = "Faker-13.12.0.tar.gz", hash = "sha256:1f6478011ac8a8273e0f9cd6da03d9ea6391c622db340eca015339512e9cde29"},
]
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"},
]
pytest-django = [
{file = "pytest-django-4.5.2.tar.gz", hash = "sha256:d9076f759bb7c36939dbdd5ae6633c18edfc2902d1a69fdbefd2426b970ce6c2"},
{file = "pytest_django-4.5.2-py3-none-any.whl", hash = "sha256:c60834861933773109334fe5a53e83d1ef4828f2203a1d6a0fa9972f4f75ab3e"},
]
python-dateutil = [
{file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"},
{file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"},
]
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"},
]
six = [
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
]
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"},
]
sqlparse = [
{file = "sqlparse-0.4.2-py3-none-any.whl", hash = "sha256:48719e356bb8b42991bdbb1e8b83223757b93789c00910a616a071910ca4a64d"},
{file = "sqlparse-0.4.2.tar.gz", hash = "sha256:0c00730c74263a94e5a9919ade150dfc3b19c574389985446148402998287dae"},
]
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,32 @@
[tool.poetry]
name = "bthlabs-jsonrpc-django"
version = "1.0.0"
description = "BTHLabs JSONRPC - Django integration"
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/aiohttp/"
[tool.poetry.dependencies]
python = "^3.10"
django = ">=3.2,<5.0"
bthlabs-jsonrpc-core = "1.0.0"
[tool.poetry.dev-dependencies]
bthlabs-jsonrpc-core = { path = "../bthlabs-jsonrpc-core", develop = true }
django = "3.2.13"
factory-boy = "3.2.1"
flake8 = "4.0.1"
flake8-commas = "2.1.0"
mypy = "0.950"
pytest = "7.1.2"
pytest-django = "4.5.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,7 @@
[flake8]
exclude = .venv/,.pytest_cache/,example/*/migrations/*.py,testing/migrations/*.py
ignore = E402
max-line-length = 119
[tool:pytest]
DJANGO_SETTINGS_MODULE = testing.settings

View File

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

View File

@ -0,0 +1 @@
/db.sqlite3

View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class TestingConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'testing'

View File

@ -0,0 +1,14 @@
# -*- coding: utf-8 -*-
import factory
from testing.models import Thing
class ThingFactory(factory.django.DjangoModelFactory):
name = factory.Faker('name')
content = factory.Faker('sentence')
is_active = True
owner = None
class Meta:
model = Thing

View File

@ -0,0 +1,33 @@
# Generated by Django 4.0.4 on 2022-05-13 06:44
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Thing',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255)),
('content', models.TextField()),
('created_at', models.DateTimeField(auto_now_add=True)),
('modified_at', models.DateTimeField(auto_now=True)),
('is_active', models.BooleanField(db_index=True, default=True)),
('owner', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'thing',
'verbose_name_plural': 'things',
},
),
]

View File

@ -0,0 +1,29 @@
# -*- coding: utf-8 -*-
from django.db import models
class Thing(models.Model):
name = models.CharField(max_length=255)
content = models.TextField()
created_at = models.DateTimeField(auto_now=False, auto_now_add=True)
modified_at = models.DateTimeField(auto_now=True, auto_now_add=False)
is_active = models.BooleanField(default=True, db_index=True)
owner = models.ForeignKey(
'auth.User', null=True, blank=True, on_delete=models.CASCADE,
)
class Meta:
verbose_name = 'thing'
verbose_name_plural = 'things'
def to_rpc(self):
return {
'id': self.pk,
'name': self.name,
'content': self.content,
'created_at': self.created_at,
'modified_at': self.modified_at,
'is_active': self.is_active,
'owner_id': self.owner_id,
}

View File

@ -0,0 +1,77 @@
# -*- coding: utf-8 -*-
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent
SECRET_KEY = 'bthlabs_jsonrpc_django'
DEBUG = False
ALLOWED_HOSTS = ['*']
INSTALLED_APPS = [
# Django apps
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# 3rd party apps
'bthlabs_jsonrpc_django',
# Project apps
'testing',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'testing.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
},
}
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
]
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_TZ = True
STATIC_URL = 'static/'
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
JSONRPC_METHOD_MODULES = [
]

View File

@ -0,0 +1,15 @@
# -*- coding: utf-8 -*-
from django.urls import path
from bthlabs_jsonrpc_django import JSONRPCView, is_authenticated
urlpatterns = [
path(
'rpc/private',
JSONRPCView.as_view(
auth_checks=[is_authenticated],
namespace='private',
),
),
path('rpc', JSONRPCView.as_view()),
]

Some files were not shown because too many files have changed in this diff Show More