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:
commit
c75ea4ea9d
14
.gitignore
vendored
Normal file
14
.gitignore
vendored
Normal 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
12
README.md
Normal 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.
|
19
packages/bthlabs-jsonrpc-aiohttp/LICENSE
Normal file
19
packages/bthlabs-jsonrpc-aiohttp/LICENSE
Normal 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.
|
55
packages/bthlabs-jsonrpc-aiohttp/README.rst
Normal file
55
packages/bthlabs-jsonrpc-aiohttp/README.rst
Normal 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/
|
|
@ -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'
|
|
@ -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
|
|
@ -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)
|
20
packages/bthlabs-jsonrpc-aiohttp/docs/Makefile
Normal file
20
packages/bthlabs-jsonrpc-aiohttp/docs/Makefile
Normal 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)
|
13
packages/bthlabs-jsonrpc-aiohttp/docs/source/api.rst
Normal file
13
packages/bthlabs-jsonrpc-aiohttp/docs/source/api.rst
Normal 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__
|
57
packages/bthlabs-jsonrpc-aiohttp/docs/source/conf.py
Normal file
57
packages/bthlabs-jsonrpc-aiohttp/docs/source/conf.py
Normal 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']
|
17
packages/bthlabs-jsonrpc-aiohttp/docs/source/index.rst
Normal file
17
packages/bthlabs-jsonrpc-aiohttp/docs/source/index.rst
Normal 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
|
36
packages/bthlabs-jsonrpc-aiohttp/docs/source/overview.rst
Normal file
36
packages/bthlabs-jsonrpc-aiohttp/docs/source/overview.rst
Normal 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}!'
|
63
packages/bthlabs-jsonrpc-aiohttp/example/example.py
Normal file
63
packages/bthlabs-jsonrpc-aiohttp/example/example.py
Normal 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()
|
3
packages/bthlabs-jsonrpc-aiohttp/example/start.sh
Executable file
3
packages/bthlabs-jsonrpc-aiohttp/example/start.sh
Executable file
|
@ -0,0 +1,3 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# django-jsonrpc-aiohttp | (c) 2022-present Tomek Wójcik | MIT License
|
||||||
|
exec adev runserver example.py
|
1213
packages/bthlabs-jsonrpc-aiohttp/poetry.lock
generated
Normal file
1213
packages/bthlabs-jsonrpc-aiohttp/poetry.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
3
packages/bthlabs-jsonrpc-aiohttp/poetry.toml
Normal file
3
packages/bthlabs-jsonrpc-aiohttp/poetry.toml
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
[virtualenvs]
|
||||||
|
create = true
|
||||||
|
in-project = true
|
32
packages/bthlabs-jsonrpc-aiohttp/pyproject.toml
Normal file
32
packages/bthlabs-jsonrpc-aiohttp/pyproject.toml
Normal 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"
|
7
packages/bthlabs-jsonrpc-aiohttp/setup.cfg
Normal file
7
packages/bthlabs-jsonrpc-aiohttp/setup.cfg
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
[flake8]
|
||||||
|
exclude = .venv/,.pytest_cache/
|
||||||
|
ignore = E402
|
||||||
|
max-line-length = 119
|
||||||
|
|
||||||
|
[tool:pytest]
|
||||||
|
asyncio_mode = auto
|
2
packages/bthlabs-jsonrpc-aiohttp/skel/envrc
Normal file
2
packages/bthlabs-jsonrpc-aiohttp/skel/envrc
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
export VIRTUAL_ENV="`realpath .venv`"
|
||||||
|
export PATH="$VIRTUAL_ENV/bin:$PATH"
|
0
packages/bthlabs-jsonrpc-aiohttp/tests/__init__.py
Normal file
0
packages/bthlabs-jsonrpc-aiohttp/tests/__init__.py
Normal file
10
packages/bthlabs-jsonrpc-aiohttp/tests/conftest.py
Normal file
10
packages/bthlabs-jsonrpc-aiohttp/tests/conftest.py
Normal 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)
|
|
@ -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},
|
||||||
|
)
|
148
packages/bthlabs-jsonrpc-aiohttp/tests/views/test_JSONRPCView.py
Normal file
148
packages/bthlabs-jsonrpc-aiohttp/tests/views/test_JSONRPCView.py
Normal 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
|
19
packages/bthlabs-jsonrpc-core/LICENSE
Normal file
19
packages/bthlabs-jsonrpc-core/LICENSE
Normal 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.
|
57
packages/bthlabs-jsonrpc-core/README.rst
Normal file
57
packages/bthlabs-jsonrpc-core/README.rst
Normal 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/
|
|
@ -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'
|
|
@ -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
|
|
@ -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'
|
397
packages/bthlabs-jsonrpc-core/bthlabs_jsonrpc_core/executor.py
Normal file
397
packages/bthlabs-jsonrpc-core/bthlabs_jsonrpc_core/executor.py
Normal 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
|
|
@ -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)
|
181
packages/bthlabs-jsonrpc-core/bthlabs_jsonrpc_core/serializer.py
Normal file
181
packages/bthlabs-jsonrpc-core/bthlabs_jsonrpc_core/serializer.py
Normal 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
|
20
packages/bthlabs-jsonrpc-core/docs/Makefile
Normal file
20
packages/bthlabs-jsonrpc-core/docs/Makefile
Normal 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)
|
43
packages/bthlabs-jsonrpc-core/docs/source/api.rst
Normal file
43
packages/bthlabs-jsonrpc-core/docs/source/api.rst
Normal 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:
|
57
packages/bthlabs-jsonrpc-core/docs/source/conf.py
Normal file
57
packages/bthlabs-jsonrpc-core/docs/source/conf.py
Normal 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']
|
18
packages/bthlabs-jsonrpc-core/docs/source/index.rst
Normal file
18
packages/bthlabs-jsonrpc-core/docs/source/index.rst
Normal 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
|
30
packages/bthlabs-jsonrpc-core/docs/source/integrations.rst
Normal file
30
packages/bthlabs-jsonrpc-core/docs/source/integrations.rst
Normal 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/ |
|
||||||
|
+-------------------+------------------------------------------------------+
|
89
packages/bthlabs-jsonrpc-core/docs/source/overview.rst
Normal file
89
packages/bthlabs-jsonrpc-core/docs/source/overview.rst
Normal 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
672
packages/bthlabs-jsonrpc-core/poetry.lock
generated
Normal 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"},
|
||||||
|
]
|
3
packages/bthlabs-jsonrpc-core/poetry.toml
Normal file
3
packages/bthlabs-jsonrpc-core/poetry.toml
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
[virtualenvs]
|
||||||
|
create = true
|
||||||
|
in-project = true
|
26
packages/bthlabs-jsonrpc-core/pyproject.toml
Normal file
26
packages/bthlabs-jsonrpc-core/pyproject.toml
Normal 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"
|
4
packages/bthlabs-jsonrpc-core/setup.cfg
Normal file
4
packages/bthlabs-jsonrpc-core/setup.cfg
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
[flake8]
|
||||||
|
exclude = .venv/,.pytest_cache/
|
||||||
|
ignore = E402
|
||||||
|
max-line-length = 119
|
2
packages/bthlabs-jsonrpc-core/skel/envrc
Normal file
2
packages/bthlabs-jsonrpc-core/skel/envrc
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
export VIRTUAL_ENV="`realpath .venv`"
|
||||||
|
export PATH="$VIRTUAL_ENV/bin:$PATH"
|
0
packages/bthlabs-jsonrpc-core/tests/__init__.py
Normal file
0
packages/bthlabs-jsonrpc-core/tests/__init__.py
Normal file
22
packages/bthlabs-jsonrpc-core/tests/conftest.py
Normal file
22
packages/bthlabs-jsonrpc-core/tests/conftest.py
Normal 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)
|
|
@ -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,
|
||||||
|
)
|
|
@ -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
|
776
packages/bthlabs-jsonrpc-core/tests/executor/test_Executor.py
Normal file
776
packages/bthlabs-jsonrpc-core/tests/executor/test_Executor.py
Normal 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},
|
||||||
|
)
|
|
@ -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
|
|
@ -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'
|
19
packages/bthlabs-jsonrpc-django/LICENSE
Normal file
19
packages/bthlabs-jsonrpc-django/LICENSE
Normal 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.
|
71
packages/bthlabs-jsonrpc-django/README.rst
Normal file
71
packages/bthlabs-jsonrpc-django/README.rst
Normal 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/
|
|
@ -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'
|
|
@ -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)
|
|
@ -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
|
|
@ -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')
|
|
@ -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)
|
122
packages/bthlabs-jsonrpc-django/bthlabs_jsonrpc_django/views.py
Normal file
122
packages/bthlabs-jsonrpc-django/bthlabs_jsonrpc_django/views.py
Normal 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
|
20
packages/bthlabs-jsonrpc-django/docs/Makefile
Normal file
20
packages/bthlabs-jsonrpc-django/docs/Makefile
Normal 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)
|
21
packages/bthlabs-jsonrpc-django/docs/source/api.rst
Normal file
21
packages/bthlabs-jsonrpc-django/docs/source/api.rst
Normal 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
|
57
packages/bthlabs-jsonrpc-django/docs/source/conf.py
Normal file
57
packages/bthlabs-jsonrpc-django/docs/source/conf.py
Normal 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']
|
17
packages/bthlabs-jsonrpc-django/docs/source/index.rst
Normal file
17
packages/bthlabs-jsonrpc-django/docs/source/index.rst
Normal 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
|
57
packages/bthlabs-jsonrpc-django/docs/source/overview.rst
Normal file
57
packages/bthlabs-jsonrpc-django/docs/source/overview.rst
Normal 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}!'
|
1
packages/bthlabs-jsonrpc-django/example/.gitignore
vendored
Normal file
1
packages/bthlabs-jsonrpc-django/example/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
/db.sqlite3
|
|
@ -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()
|
|
@ -0,0 +1,6 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from django.contrib.auth.middleware import RemoteUserMiddleware
|
||||||
|
|
||||||
|
|
||||||
|
class CustomHeaderRemoteUserMiddleware(RemoteUserMiddleware):
|
||||||
|
header = 'HTTP_X_USER'
|
1
packages/bthlabs-jsonrpc-django/example/bthlabs_jsonrpc_django_example/settings/.gitignore
vendored
Normal file
1
packages/bthlabs-jsonrpc-django/example/bthlabs_jsonrpc_django_example/settings/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
/local.py
|
|
@ -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',
|
||||||
|
]
|
|
@ -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)
|
|
@ -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'
|
|
@ -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',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
|
@ -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,
|
||||||
|
}
|
|
@ -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
|
|
@ -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()),
|
||||||
|
]
|
|
@ -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()
|
27
packages/bthlabs-jsonrpc-django/example/manage.py
Executable file
27
packages/bthlabs-jsonrpc-django/example/manage.py
Executable 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()
|
814
packages/bthlabs-jsonrpc-django/poetry.lock
generated
Normal file
814
packages/bthlabs-jsonrpc-django/poetry.lock
generated
Normal 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"},
|
||||||
|
]
|
3
packages/bthlabs-jsonrpc-django/poetry.toml
Normal file
3
packages/bthlabs-jsonrpc-django/poetry.toml
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
[virtualenvs]
|
||||||
|
create = true
|
||||||
|
in-project = true
|
32
packages/bthlabs-jsonrpc-django/pyproject.toml
Normal file
32
packages/bthlabs-jsonrpc-django/pyproject.toml
Normal 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"
|
7
packages/bthlabs-jsonrpc-django/setup.cfg
Normal file
7
packages/bthlabs-jsonrpc-django/setup.cfg
Normal 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
|
2
packages/bthlabs-jsonrpc-django/skel/envrc
Normal file
2
packages/bthlabs-jsonrpc-django/skel/envrc
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
export VIRTUAL_ENV="`realpath .venv`"
|
||||||
|
export PATH="$VIRTUAL_ENV/bin:$PATH"
|
1
packages/bthlabs-jsonrpc-django/testing/.gitignore
vendored
Normal file
1
packages/bthlabs-jsonrpc-django/testing/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
/db.sqlite3
|
0
packages/bthlabs-jsonrpc-django/testing/__init__.py
Normal file
0
packages/bthlabs-jsonrpc-django/testing/__init__.py
Normal file
6
packages/bthlabs-jsonrpc-django/testing/apps.py
Normal file
6
packages/bthlabs-jsonrpc-django/testing/apps.py
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class TestingConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'testing'
|
14
packages/bthlabs-jsonrpc-django/testing/factories.py
Normal file
14
packages/bthlabs-jsonrpc-django/testing/factories.py
Normal 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
|
|
@ -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',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
29
packages/bthlabs-jsonrpc-django/testing/models.py
Normal file
29
packages/bthlabs-jsonrpc-django/testing/models.py
Normal 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,
|
||||||
|
}
|
77
packages/bthlabs-jsonrpc-django/testing/settings.py
Normal file
77
packages/bthlabs-jsonrpc-django/testing/settings.py
Normal 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 = [
|
||||||
|
]
|
15
packages/bthlabs-jsonrpc-django/testing/urls.py
Normal file
15
packages/bthlabs-jsonrpc-django/testing/urls.py
Normal 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
Loading…
Reference in New Issue
Block a user