commit c75ea4ea9dc18cfa9d456932b123e136a36a6a31 Author: Tomek Wójcik Date: Sat Jun 4 10:41:53 2022 +0200 Initial public releases * `bthlabs-jsonrpc-aiohttp` v1.0.0 * `bthlabs-jsonrpc-core` v1.0.0 * `bthlabs-jsonrpc-django` v1.0.0 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5d7857d --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +*.pyc +*.pyo +*.swp + +.envrc +.venv +invoke.json +tasks.py + +.mypy_cache/ +.pytest_cache/ +build/ +dist/ +ops/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..3f8bcb3 --- /dev/null +++ b/README.md @@ -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. diff --git a/packages/bthlabs-jsonrpc-aiohttp/LICENSE b/packages/bthlabs-jsonrpc-aiohttp/LICENSE new file mode 100644 index 0000000..d5b80fc --- /dev/null +++ b/packages/bthlabs-jsonrpc-aiohttp/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2022-present Tomek Wójcik + +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. diff --git a/packages/bthlabs-jsonrpc-aiohttp/README.rst b/packages/bthlabs-jsonrpc-aiohttp/README.rst new file mode 100644 index 0000000..8788eb9 --- /dev/null +++ b/packages/bthlabs-jsonrpc-aiohttp/README.rst @@ -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/ diff --git a/packages/bthlabs-jsonrpc-aiohttp/bthlabs_jsonrpc_aiohttp/__init__.py b/packages/bthlabs-jsonrpc-aiohttp/bthlabs_jsonrpc_aiohttp/__init__.py new file mode 100644 index 0000000..5aa1b54 --- /dev/null +++ b/packages/bthlabs-jsonrpc-aiohttp/bthlabs_jsonrpc_aiohttp/__init__.py @@ -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' diff --git a/packages/bthlabs-jsonrpc-aiohttp/bthlabs_jsonrpc_aiohttp/executor.py b/packages/bthlabs-jsonrpc-aiohttp/bthlabs_jsonrpc_aiohttp/executor.py new file mode 100644 index 0000000..439ec39 --- /dev/null +++ b/packages/bthlabs-jsonrpc-aiohttp/bthlabs_jsonrpc_aiohttp/executor.py @@ -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 diff --git a/packages/bthlabs-jsonrpc-aiohttp/bthlabs_jsonrpc_aiohttp/views.py b/packages/bthlabs-jsonrpc-aiohttp/bthlabs_jsonrpc_aiohttp/views.py new file mode 100644 index 0000000..56fed12 --- /dev/null +++ b/packages/bthlabs-jsonrpc-aiohttp/bthlabs_jsonrpc_aiohttp/views.py @@ -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) diff --git a/packages/bthlabs-jsonrpc-aiohttp/docs/Makefile b/packages/bthlabs-jsonrpc-aiohttp/docs/Makefile new file mode 100644 index 0000000..d0c3cbf --- /dev/null +++ b/packages/bthlabs-jsonrpc-aiohttp/docs/Makefile @@ -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) diff --git a/packages/bthlabs-jsonrpc-aiohttp/docs/source/api.rst b/packages/bthlabs-jsonrpc-aiohttp/docs/source/api.rst new file mode 100644 index 0000000..15ccc6e --- /dev/null +++ b/packages/bthlabs-jsonrpc-aiohttp/docs/source/api.rst @@ -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__ diff --git a/packages/bthlabs-jsonrpc-aiohttp/docs/source/conf.py b/packages/bthlabs-jsonrpc-aiohttp/docs/source/conf.py new file mode 100644 index 0000000..ced513e --- /dev/null +++ b/packages/bthlabs-jsonrpc-aiohttp/docs/source/conf.py @@ -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'] diff --git a/packages/bthlabs-jsonrpc-aiohttp/docs/source/index.rst b/packages/bthlabs-jsonrpc-aiohttp/docs/source/index.rst new file mode 100644 index 0000000..8d93f25 --- /dev/null +++ b/packages/bthlabs-jsonrpc-aiohttp/docs/source/index.rst @@ -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 diff --git a/packages/bthlabs-jsonrpc-aiohttp/docs/source/overview.rst b/packages/bthlabs-jsonrpc-aiohttp/docs/source/overview.rst new file mode 100644 index 0000000..1c5f4f5 --- /dev/null +++ b/packages/bthlabs-jsonrpc-aiohttp/docs/source/overview.rst @@ -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}!' diff --git a/packages/bthlabs-jsonrpc-aiohttp/example/example.py b/packages/bthlabs-jsonrpc-aiohttp/example/example.py new file mode 100644 index 0000000..c557744 --- /dev/null +++ b/packages/bthlabs-jsonrpc-aiohttp/example/example.py @@ -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() diff --git a/packages/bthlabs-jsonrpc-aiohttp/example/start.sh b/packages/bthlabs-jsonrpc-aiohttp/example/start.sh new file mode 100755 index 0000000..35f0b56 --- /dev/null +++ b/packages/bthlabs-jsonrpc-aiohttp/example/start.sh @@ -0,0 +1,3 @@ +#!/bin/bash +# django-jsonrpc-aiohttp | (c) 2022-present Tomek Wójcik | MIT License +exec adev runserver example.py diff --git a/packages/bthlabs-jsonrpc-aiohttp/poetry.lock b/packages/bthlabs-jsonrpc-aiohttp/poetry.lock new file mode 100644 index 0000000..088c794 --- /dev/null +++ b/packages/bthlabs-jsonrpc-aiohttp/poetry.lock @@ -0,0 +1,1213 @@ +[[package]] +name = "aiohttp" +version = "3.8.1" +description = "Async http client/server framework (asyncio)" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +aiosignal = ">=1.1.2" +async-timeout = ">=4.0.0a3,<5.0" +attrs = ">=17.3.0" +charset-normalizer = ">=2.0,<3.0" +frozenlist = ">=1.1.1" +multidict = ">=4.5,<7.0" +yarl = ">=1.0,<2.0" + +[package.extras] +speedups = ["aiodns", "brotli", "cchardet"] + +[[package]] +name = "aiohttp-devtools" +version = "1.0.post0" +description = "Dev tools for aiohttp" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +aiohttp = ">=3.8.0" +click = ">=6.6" +devtools = ">=0.5" +Pygments = ">=2.2.0" +watchgod = ">=0.2" + +[[package]] +name = "aiosignal" +version = "1.2.0" +description = "aiosignal: a list of registered asynchronous callbacks" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +frozenlist = ">=1.1.0" + +[[package]] +name = "alabaster" +version = "0.7.12" +description = "A configurable sidebar-enabled Sphinx theme" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "anyio" +version = "3.6.1" +description = "High level compatibility layer for multiple asynchronous event loop implementations" +category = "dev" +optional = false +python-versions = ">=3.6.2" + +[package.dependencies] +idna = ">=2.8" +sniffio = ">=1.1" + +[package.extras] +doc = ["packaging", "sphinx-rtd-theme", "sphinx-autodoc-typehints (>=1.2.0)"] +test = ["coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "contextlib2", "uvloop (<0.15)", "mock (>=4)", "uvloop (>=0.15)"] +trio = ["trio (>=0.16)"] + +[[package]] +name = "asttokens" +version = "2.0.5" +description = "Annotate AST trees with source code positions" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +six = "*" + +[package.extras] +test = ["astroid", "pytest"] + +[[package]] +name = "async-timeout" +version = "4.0.2" +description = "Timeout context manager for asyncio programs" +category = "main" +optional = false +python-versions = ">=3.6" + +[[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 = "main" +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 = "main" +optional = false +python-versions = ">=3.5.0" + +[package.extras] +unicode_backport = ["unicodedata2"] + +[[package]] +name = "click" +version = "8.1.3" +description = "Composable command line interface toolkit" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[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 = "devtools" +version = "0.8.0" +description = "Python's missing debug print command and other development tools." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +asttokens = ">=2.0.0,<3.0.0" +executing = ">=0.8.0,<1.0.0" + +[package.extras] +pygments = ["Pygments (>=2.2.0)"] + +[[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 = "executing" +version = "0.8.3" +description = "Get the currently executing AST node of a frame, and other information" +category = "dev" +optional = false +python-versions = "*" + +[[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 = "frozenlist" +version = "1.3.0" +description = "A list-like structure which implements collections.abc.MutableSequence" +category = "main" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "idna" +version = "3.3" +description = "Internationalized Domain Names in Applications (IDNA)" +category = "main" +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 = "multidict" +version = "6.0.2" +description = "multidict implementation" +category = "main" +optional = false +python-versions = ">=3.7" + +[[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-aiohttp" +version = "1.0.4" +description = "Pytest plugin for aiohttp support" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +aiohttp = ">=3.8.1" +pytest = ">=6.1.0" +pytest-asyncio = ">=0.17.2" + +[package.extras] +testing = ["coverage (==6.2)", "mypy (==0.931)"] + +[[package]] +name = "pytest-asyncio" +version = "0.18.3" +description = "Pytest support for asyncio" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +pytest = ">=6.1.0" + +[package.extras] +testing = ["coverage (==6.2)", "hypothesis (>=5.7.1)", "flaky (>=3.5.0)", "mypy (==0.931)", "pytest-trio (>=0.7.0)"] + +[[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 = "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 = "sniffio" +version = "1.2.0" +description = "Sniff out which async library your code is running under" +category = "dev" +optional = false +python-versions = ">=3.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)"] + +[[package]] +name = "watchgod" +version = "0.8.2" +description = "Simple, modern file watching and code reload in python." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +anyio = ">=3.0.0,<4" + +[[package]] +name = "yarl" +version = "1.7.2" +description = "Yet another URL library" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +idna = ">=2.0" +multidict = ">=4.0" + +[metadata] +lock-version = "1.1" +python-versions = "^3.10" +content-hash = "714e99a73100ba68deeb17fcec9dde1e2ca00b074477e2163727e372c8b287a5" + +[metadata.files] +aiohttp = [ + {file = "aiohttp-3.8.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1ed0b6477896559f17b9eaeb6d38e07f7f9ffe40b9f0f9627ae8b9926ae260a8"}, + {file = "aiohttp-3.8.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7dadf3c307b31e0e61689cbf9e06be7a867c563d5a63ce9dca578f956609abf8"}, + {file = "aiohttp-3.8.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a79004bb58748f31ae1cbe9fa891054baaa46fb106c2dc7af9f8e3304dc30316"}, + {file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:12de6add4038df8f72fac606dff775791a60f113a725c960f2bab01d8b8e6b15"}, + {file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6f0d5f33feb5f69ddd57a4a4bd3d56c719a141080b445cbf18f238973c5c9923"}, + {file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eaba923151d9deea315be1f3e2b31cc39a6d1d2f682f942905951f4e40200922"}, + {file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:099ebd2c37ac74cce10a3527d2b49af80243e2a4fa39e7bce41617fbc35fa3c1"}, + {file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2e5d962cf7e1d426aa0e528a7e198658cdc8aa4fe87f781d039ad75dcd52c516"}, + {file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:fa0ffcace9b3aa34d205d8130f7873fcfefcb6a4dd3dd705b0dab69af6712642"}, + {file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:61bfc23df345d8c9716d03717c2ed5e27374e0fe6f659ea64edcd27b4b044cf7"}, + {file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:31560d268ff62143e92423ef183680b9829b1b482c011713ae941997921eebc8"}, + {file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:01d7bdb774a9acc838e6b8f1d114f45303841b89b95984cbb7d80ea41172a9e3"}, + {file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:97ef77eb6b044134c0b3a96e16abcb05ecce892965a2124c566af0fd60f717e2"}, + {file = "aiohttp-3.8.1-cp310-cp310-win32.whl", hash = "sha256:c2aef4703f1f2ddc6df17519885dbfa3514929149d3ff900b73f45998f2532fa"}, + {file = "aiohttp-3.8.1-cp310-cp310-win_amd64.whl", hash = "sha256:713ac174a629d39b7c6a3aa757b337599798da4c1157114a314e4e391cd28e32"}, + {file = "aiohttp-3.8.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:473d93d4450880fe278696549f2e7aed8cd23708c3c1997981464475f32137db"}, + {file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99b5eeae8e019e7aad8af8bb314fb908dd2e028b3cdaad87ec05095394cce632"}, + {file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3af642b43ce56c24d063325dd2cf20ee012d2b9ba4c3c008755a301aaea720ad"}, + {file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c3630c3ef435c0a7c549ba170a0633a56e92629aeed0e707fec832dee313fb7a"}, + {file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4a4a4e30bf1edcad13fb0804300557aedd07a92cabc74382fdd0ba6ca2661091"}, + {file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6f8b01295e26c68b3a1b90efb7a89029110d3a4139270b24fda961893216c440"}, + {file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:a25fa703a527158aaf10dafd956f7d42ac6d30ec80e9a70846253dd13e2f067b"}, + {file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:5bfde62d1d2641a1f5173b8c8c2d96ceb4854f54a44c23102e2ccc7e02f003ec"}, + {file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:51467000f3647d519272392f484126aa716f747859794ac9924a7aafa86cd411"}, + {file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:03a6d5349c9ee8f79ab3ff3694d6ce1cfc3ced1c9d36200cb8f08ba06bd3b782"}, + {file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:102e487eeb82afac440581e5d7f8f44560b36cf0bdd11abc51a46c1cd88914d4"}, + {file = "aiohttp-3.8.1-cp36-cp36m-win32.whl", hash = "sha256:4aed991a28ea3ce320dc8ce655875e1e00a11bdd29fe9444dd4f88c30d558602"}, + {file = "aiohttp-3.8.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b0e20cddbd676ab8a64c774fefa0ad787cc506afd844de95da56060348021e96"}, + {file = "aiohttp-3.8.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:37951ad2f4a6df6506750a23f7cbabad24c73c65f23f72e95897bb2cecbae676"}, + {file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c23b1ad869653bc818e972b7a3a79852d0e494e9ab7e1a701a3decc49c20d51"}, + {file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:15b09b06dae900777833fe7fc4b4aa426556ce95847a3e8d7548e2d19e34edb8"}, + {file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:477c3ea0ba410b2b56b7efb072c36fa91b1e6fc331761798fa3f28bb224830dd"}, + {file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2f2f69dca064926e79997f45b2f34e202b320fd3782f17a91941f7eb85502ee2"}, + {file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ef9612483cb35171d51d9173647eed5d0069eaa2ee812793a75373447d487aa4"}, + {file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6d69f36d445c45cda7b3b26afef2fc34ef5ac0cdc75584a87ef307ee3c8c6d00"}, + {file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:55c3d1072704d27401c92339144d199d9de7b52627f724a949fc7d5fc56d8b93"}, + {file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:b9d00268fcb9f66fbcc7cd9fe423741d90c75ee029a1d15c09b22d23253c0a44"}, + {file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:07b05cd3305e8a73112103c834e91cd27ce5b4bd07850c4b4dbd1877d3f45be7"}, + {file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c34dc4958b232ef6188c4318cb7b2c2d80521c9a56c52449f8f93ab7bc2a8a1c"}, + {file = "aiohttp-3.8.1-cp37-cp37m-win32.whl", hash = "sha256:d2f9b69293c33aaa53d923032fe227feac867f81682f002ce33ffae978f0a9a9"}, + {file = "aiohttp-3.8.1-cp37-cp37m-win_amd64.whl", hash = "sha256:6ae828d3a003f03ae31915c31fa684b9890ea44c9c989056fea96e3d12a9fa17"}, + {file = "aiohttp-3.8.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0c7ebbbde809ff4e970824b2b6cb7e4222be6b95a296e46c03cf050878fc1785"}, + {file = "aiohttp-3.8.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8b7ef7cbd4fec9a1e811a5de813311ed4f7ac7d93e0fda233c9b3e1428f7dd7b"}, + {file = "aiohttp-3.8.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c3d6a4d0619e09dcd61021debf7059955c2004fa29f48788a3dfaf9c9901a7cd"}, + {file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:718626a174e7e467f0558954f94af117b7d4695d48eb980146016afa4b580b2e"}, + {file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:589c72667a5febd36f1315aa6e5f56dd4aa4862df295cb51c769d16142ddd7cd"}, + {file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2ed076098b171573161eb146afcb9129b5ff63308960aeca4b676d9d3c35e700"}, + {file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:086f92daf51a032d062ec5f58af5ca6a44d082c35299c96376a41cbb33034675"}, + {file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:11691cf4dc5b94236ccc609b70fec991234e7ef8d4c02dd0c9668d1e486f5abf"}, + {file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:31d1e1c0dbf19ebccbfd62eff461518dcb1e307b195e93bba60c965a4dcf1ba0"}, + {file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:11a67c0d562e07067c4e86bffc1553f2cf5b664d6111c894671b2b8712f3aba5"}, + {file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:bb01ba6b0d3f6c68b89fce7305080145d4877ad3acaed424bae4d4ee75faa950"}, + {file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:44db35a9e15d6fe5c40d74952e803b1d96e964f683b5a78c3cc64eb177878155"}, + {file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:844a9b460871ee0a0b0b68a64890dae9c415e513db0f4a7e3cab41a0f2fedf33"}, + {file = "aiohttp-3.8.1-cp38-cp38-win32.whl", hash = "sha256:7d08744e9bae2ca9c382581f7dce1273fe3c9bae94ff572c3626e8da5b193c6a"}, + {file = "aiohttp-3.8.1-cp38-cp38-win_amd64.whl", hash = "sha256:04d48b8ce6ab3cf2097b1855e1505181bdd05586ca275f2505514a6e274e8e75"}, + {file = "aiohttp-3.8.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:f5315a2eb0239185af1bddb1abf472d877fede3cc8d143c6cddad37678293237"}, + {file = "aiohttp-3.8.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a996d01ca39b8dfe77440f3cd600825d05841088fd6bc0144cc6c2ec14cc5f74"}, + {file = "aiohttp-3.8.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:13487abd2f761d4be7c8ff9080de2671e53fff69711d46de703c310c4c9317ca"}, + {file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea302f34477fda3f85560a06d9ebdc7fa41e82420e892fc50b577e35fc6a50b2"}, + {file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a2f635ce61a89c5732537a7896b6319a8fcfa23ba09bec36e1b1ac0ab31270d2"}, + {file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e999f2d0e12eea01caeecb17b653f3713d758f6dcc770417cf29ef08d3931421"}, + {file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0770e2806a30e744b4e21c9d73b7bee18a1cfa3c47991ee2e5a65b887c49d5cf"}, + {file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d15367ce87c8e9e09b0f989bfd72dc641bcd04ba091c68cd305312d00962addd"}, + {file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6c7cefb4b0640703eb1069835c02486669312bf2f12b48a748e0a7756d0de33d"}, + {file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:71927042ed6365a09a98a6377501af5c9f0a4d38083652bcd2281a06a5976724"}, + {file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:28d490af82bc6b7ce53ff31337a18a10498303fe66f701ab65ef27e143c3b0ef"}, + {file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:b6613280ccedf24354406caf785db748bebbddcf31408b20c0b48cb86af76866"}, + {file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:81e3d8c34c623ca4e36c46524a3530e99c0bc95ed068fd6e9b55cb721d408fb2"}, + {file = "aiohttp-3.8.1-cp39-cp39-win32.whl", hash = "sha256:7187a76598bdb895af0adbd2fb7474d7f6025d170bc0a1130242da817ce9e7d1"}, + {file = "aiohttp-3.8.1-cp39-cp39-win_amd64.whl", hash = "sha256:1c182cb873bc91b411e184dab7a2b664d4fea2743df0e4d57402f7f3fa644bac"}, + {file = "aiohttp-3.8.1.tar.gz", hash = "sha256:fc5471e1a54de15ef71c1bc6ebe80d4dc681ea600e68bfd1cbce40427f0b7578"}, +] +aiohttp-devtools = [ + {file = "aiohttp-devtools-1.0.post0.tar.gz", hash = "sha256:f46a87b250a84bf8edca21c8b6991dc2b6145cdf0af8a08b73f5c92d48d85f01"}, + {file = "aiohttp_devtools-1.0.post0-py37.py38.py39.py310-none-any.whl", hash = "sha256:1847ce92e6e8ca1ed5dc603864f4faecdfa40b6b5aeb5193f05cafd28d3ce3e3"}, +] +aiosignal = [ + {file = "aiosignal-1.2.0-py3-none-any.whl", hash = "sha256:26e62109036cd181df6e6ad646f91f0dcfd05fe16d0cb924138ff2ab75d64e3a"}, + {file = "aiosignal-1.2.0.tar.gz", hash = "sha256:78ed67db6c7b7ced4f98e495e572106d5c432a93e1ddd1bf475e1dc05f5b7df2"}, +] +alabaster = [ + {file = "alabaster-0.7.12-py2.py3-none-any.whl", hash = "sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359"}, + {file = "alabaster-0.7.12.tar.gz", hash = "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"}, +] +anyio = [ + {file = "anyio-3.6.1-py3-none-any.whl", hash = "sha256:cb29b9c70620506a9a8f87a309591713446953302d7d995344d0d7c6c0c9a7be"}, + {file = "anyio-3.6.1.tar.gz", hash = "sha256:413adf95f93886e442aea925f3ee43baa5a765a64a0f52c6081894f9992fdd0b"}, +] +asttokens = [ + {file = "asttokens-2.0.5-py2.py3-none-any.whl", hash = "sha256:0844691e88552595a6f4a4281a9f7f79b8dd45ca4ccea82e5e05b4bbdb76705c"}, + {file = "asttokens-2.0.5.tar.gz", hash = "sha256:9a54c114f02c7a9480d56550932546a3f1fe71d8a02f1bc7ccd0ee3ee35cf4d5"}, +] +async-timeout = [ + {file = "async-timeout-4.0.2.tar.gz", hash = "sha256:2163e1640ddb52b7a8c80d0a67a08587e5d245cc9c553a74a847056bc2976b15"}, + {file = "async_timeout-4.0.2-py3-none-any.whl", hash = "sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c"}, +] +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"}, +] +click = [ + {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, + {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, +] +colorama = [ + {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, + {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, +] +devtools = [ + {file = "devtools-0.8.0-py3-none-any.whl", hash = "sha256:00717ef184223cf36c65bbd17c6eb412f8a7564f47957f9e8b2b7610661b17fb"}, + {file = "devtools-0.8.0.tar.gz", hash = "sha256:6162a2f61c70242479dff3163e7837e6a9bf32451661af1347bfa3115602af16"}, +] +docutils = [ + {file = "docutils-0.17.1-py2.py3-none-any.whl", hash = "sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61"}, + {file = "docutils-0.17.1.tar.gz", hash = "sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125"}, +] +executing = [ + {file = "executing-0.8.3-py2.py3-none-any.whl", hash = "sha256:d1eef132db1b83649a3905ca6dd8897f71ac6f8cac79a7e58a1a09cf137546c9"}, + {file = "executing-0.8.3.tar.gz", hash = "sha256:c6554e21c6b060590a6d3be4b82fb78f8f0194d809de5ea7df1c093763311501"}, +] +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"}, +] +frozenlist = [ + {file = "frozenlist-1.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d2257aaba9660f78c7b1d8fea963b68f3feffb1a9d5d05a18401ca9eb3e8d0a3"}, + {file = "frozenlist-1.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4a44ebbf601d7bac77976d429e9bdb5a4614f9f4027777f9e54fd765196e9d3b"}, + {file = "frozenlist-1.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:45334234ec30fc4ea677f43171b18a27505bfb2dba9aca4398a62692c0ea8868"}, + {file = "frozenlist-1.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47be22dc27ed933d55ee55845d34a3e4e9f6fee93039e7f8ebadb0c2f60d403f"}, + {file = "frozenlist-1.3.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:03a7dd1bfce30216a3f51a84e6dd0e4a573d23ca50f0346634916ff105ba6e6b"}, + {file = "frozenlist-1.3.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:691ddf6dc50480ce49f68441f1d16a4c3325887453837036e0fb94736eae1e58"}, + {file = "frozenlist-1.3.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bde99812f237f79eaf3f04ebffd74f6718bbd216101b35ac7955c2d47c17da02"}, + {file = "frozenlist-1.3.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a202458d1298ced3768f5a7d44301e7c86defac162ace0ab7434c2e961166e8"}, + {file = "frozenlist-1.3.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b9e3e9e365991f8cc5f5edc1fd65b58b41d0514a6a7ad95ef5c7f34eb49b3d3e"}, + {file = "frozenlist-1.3.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:04cb491c4b1c051734d41ea2552fde292f5f3a9c911363f74f39c23659c4af78"}, + {file = "frozenlist-1.3.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:436496321dad302b8b27ca955364a439ed1f0999311c393dccb243e451ff66aa"}, + {file = "frozenlist-1.3.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:754728d65f1acc61e0f4df784456106e35afb7bf39cfe37227ab00436fb38676"}, + {file = "frozenlist-1.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6eb275c6385dd72594758cbe96c07cdb9bd6becf84235f4a594bdf21e3596c9d"}, + {file = "frozenlist-1.3.0-cp310-cp310-win32.whl", hash = "sha256:e30b2f9683812eb30cf3f0a8e9f79f8d590a7999f731cf39f9105a7c4a39489d"}, + {file = "frozenlist-1.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:f7353ba3367473d1d616ee727945f439e027f0bb16ac1a750219a8344d1d5d3c"}, + {file = "frozenlist-1.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:88aafd445a233dbbf8a65a62bc3249a0acd0d81ab18f6feb461cc5a938610d24"}, + {file = "frozenlist-1.3.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4406cfabef8f07b3b3af0f50f70938ec06d9f0fc26cbdeaab431cbc3ca3caeaa"}, + {file = "frozenlist-1.3.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8cf829bd2e2956066dd4de43fd8ec881d87842a06708c035b37ef632930505a2"}, + {file = "frozenlist-1.3.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:603b9091bd70fae7be28bdb8aa5c9990f4241aa33abb673390a7f7329296695f"}, + {file = "frozenlist-1.3.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:25af28b560e0c76fa41f550eacb389905633e7ac02d6eb3c09017fa1c8cdfde1"}, + {file = "frozenlist-1.3.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94c7a8a9fc9383b52c410a2ec952521906d355d18fccc927fca52ab575ee8b93"}, + {file = "frozenlist-1.3.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:65bc6e2fece04e2145ab6e3c47428d1bbc05aede61ae365b2c1bddd94906e478"}, + {file = "frozenlist-1.3.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:3f7c935c7b58b0d78c0beea0c7358e165f95f1fd8a7e98baa40d22a05b4a8141"}, + {file = "frozenlist-1.3.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd89acd1b8bb4f31b47072615d72e7f53a948d302b7c1d1455e42622de180eae"}, + {file = "frozenlist-1.3.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:6983a31698490825171be44ffbafeaa930ddf590d3f051e397143a5045513b01"}, + {file = "frozenlist-1.3.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:adac9700675cf99e3615eb6a0eb5e9f5a4143c7d42c05cea2e7f71c27a3d0846"}, + {file = "frozenlist-1.3.0-cp37-cp37m-win32.whl", hash = "sha256:0c36e78b9509e97042ef869c0e1e6ef6429e55817c12d78245eb915e1cca7468"}, + {file = "frozenlist-1.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:57f4d3f03a18facacb2a6bcd21bccd011e3b75d463dc49f838fd699d074fabd1"}, + {file = "frozenlist-1.3.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:8c905a5186d77111f02144fab5b849ab524f1e876a1e75205cd1386a9be4b00a"}, + {file = "frozenlist-1.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b5009062d78a8c6890d50b4e53b0ddda31841b3935c1937e2ed8c1bda1c7fb9d"}, + {file = "frozenlist-1.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2fdc3cd845e5a1f71a0c3518528bfdbfe2efaf9886d6f49eacc5ee4fd9a10953"}, + {file = "frozenlist-1.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:92e650bd09b5dda929523b9f8e7f99b24deac61240ecc1a32aeba487afcd970f"}, + {file = "frozenlist-1.3.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:40dff8962b8eba91fd3848d857203f0bd704b5f1fa2b3fc9af64901a190bba08"}, + {file = "frozenlist-1.3.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:768efd082074bb203c934e83a61654ed4931ef02412c2fbdecea0cff7ecd0274"}, + {file = "frozenlist-1.3.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:006d3595e7d4108a12025ddf415ae0f6c9e736e726a5db0183326fd191b14c5e"}, + {file = "frozenlist-1.3.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:871d42623ae15eb0b0e9df65baeee6976b2e161d0ba93155411d58ff27483ad8"}, + {file = "frozenlist-1.3.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:aff388be97ef2677ae185e72dc500d19ecaf31b698986800d3fc4f399a5e30a5"}, + {file = "frozenlist-1.3.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:9f892d6a94ec5c7b785e548e42722e6f3a52f5f32a8461e82ac3e67a3bd073f1"}, + {file = "frozenlist-1.3.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:e982878792c971cbd60ee510c4ee5bf089a8246226dea1f2138aa0bb67aff148"}, + {file = "frozenlist-1.3.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:c6c321dd013e8fc20735b92cb4892c115f5cdb82c817b1e5b07f6b95d952b2f0"}, + {file = "frozenlist-1.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:30530930410855c451bea83f7b272fb1c495ed9d5cc72895ac29e91279401db3"}, + {file = "frozenlist-1.3.0-cp38-cp38-win32.whl", hash = "sha256:40ec383bc194accba825fbb7d0ef3dda5736ceab2375462f1d8672d9f6b68d07"}, + {file = "frozenlist-1.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:f20baa05eaa2bcd5404c445ec51aed1c268d62600362dc6cfe04fae34a424bd9"}, + {file = "frozenlist-1.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:0437fe763fb5d4adad1756050cbf855bbb2bf0d9385c7bb13d7a10b0dd550486"}, + {file = "frozenlist-1.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b684c68077b84522b5c7eafc1dc735bfa5b341fb011d5552ebe0968e22ed641c"}, + {file = "frozenlist-1.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:93641a51f89473837333b2f8100f3f89795295b858cd4c7d4a1f18e299dc0a4f"}, + {file = "frozenlist-1.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6d32ff213aef0fd0bcf803bffe15cfa2d4fde237d1d4838e62aec242a8362fa"}, + {file = "frozenlist-1.3.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31977f84828b5bb856ca1eb07bf7e3a34f33a5cddce981d880240ba06639b94d"}, + {file = "frozenlist-1.3.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3c62964192a1c0c30b49f403495911298810bada64e4f03249ca35a33ca0417a"}, + {file = "frozenlist-1.3.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4eda49bea3602812518765810af732229b4291d2695ed24a0a20e098c45a707b"}, + {file = "frozenlist-1.3.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acb267b09a509c1df5a4ca04140da96016f40d2ed183cdc356d237286c971b51"}, + {file = "frozenlist-1.3.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e1e26ac0a253a2907d654a37e390904426d5ae5483150ce3adedb35c8c06614a"}, + {file = "frozenlist-1.3.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f96293d6f982c58ebebb428c50163d010c2f05de0cde99fd681bfdc18d4b2dc2"}, + {file = "frozenlist-1.3.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:e84cb61b0ac40a0c3e0e8b79c575161c5300d1d89e13c0e02f76193982f066ed"}, + {file = "frozenlist-1.3.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:ff9310f05b9d9c5c4dd472983dc956901ee6cb2c3ec1ab116ecdde25f3ce4951"}, + {file = "frozenlist-1.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d26b650b71fdc88065b7a21f8ace70175bcf3b5bdba5ea22df4bfd893e795a3b"}, + {file = "frozenlist-1.3.0-cp39-cp39-win32.whl", hash = "sha256:01a73627448b1f2145bddb6e6c2259988bb8aee0fb361776ff8604b99616cd08"}, + {file = "frozenlist-1.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:772965f773757a6026dea111a15e6e2678fbd6216180f82a48a40b27de1ee2ab"}, + {file = "frozenlist-1.3.0.tar.gz", hash = "sha256:ce6f2ba0edb7b0c1d8976565298ad2deba6f8064d2bebb6ffce2ca896eb35b0b"}, +] +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"}, +] +multidict = [ + {file = "multidict-6.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b9e95a740109c6047602f4db4da9949e6c5945cefbad34a1299775ddc9a62e2"}, + {file = "multidict-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac0e27844758d7177989ce406acc6a83c16ed4524ebc363c1f748cba184d89d3"}, + {file = "multidict-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:041b81a5f6b38244b34dc18c7b6aba91f9cdaf854d9a39e5ff0b58e2b5773b9c"}, + {file = "multidict-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5fdda29a3c7e76a064f2477c9aab1ba96fd94e02e386f1e665bca1807fc5386f"}, + {file = "multidict-6.0.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3368bf2398b0e0fcbf46d85795adc4c259299fec50c1416d0f77c0a843a3eed9"}, + {file = "multidict-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f4f052ee022928d34fe1f4d2bc743f32609fb79ed9c49a1710a5ad6b2198db20"}, + {file = "multidict-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:225383a6603c086e6cef0f2f05564acb4f4d5f019a4e3e983f572b8530f70c88"}, + {file = "multidict-6.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50bd442726e288e884f7be9071016c15a8742eb689a593a0cac49ea093eef0a7"}, + {file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:47e6a7e923e9cada7c139531feac59448f1f47727a79076c0b1ee80274cd8eee"}, + {file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:0556a1d4ea2d949efe5fd76a09b4a82e3a4a30700553a6725535098d8d9fb672"}, + {file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:626fe10ac87851f4cffecee161fc6f8f9853f0f6f1035b59337a51d29ff3b4f9"}, + {file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:8064b7c6f0af936a741ea1efd18690bacfbae4078c0c385d7c3f611d11f0cf87"}, + {file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2d36e929d7f6a16d4eb11b250719c39560dd70545356365b494249e2186bc389"}, + {file = "multidict-6.0.2-cp310-cp310-win32.whl", hash = "sha256:fcb91630817aa8b9bc4a74023e4198480587269c272c58b3279875ed7235c293"}, + {file = "multidict-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:8cbf0132f3de7cc6c6ce00147cc78e6439ea736cee6bca4f068bcf892b0fd658"}, + {file = "multidict-6.0.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:05f6949d6169878a03e607a21e3b862eaf8e356590e8bdae4227eedadacf6e51"}, + {file = "multidict-6.0.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2c2e459f7050aeb7c1b1276763364884595d47000c1cddb51764c0d8976e608"}, + {file = "multidict-6.0.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d0509e469d48940147e1235d994cd849a8f8195e0bca65f8f5439c56e17872a3"}, + {file = "multidict-6.0.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:514fe2b8d750d6cdb4712346a2c5084a80220821a3e91f3f71eec11cf8d28fd4"}, + {file = "multidict-6.0.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19adcfc2a7197cdc3987044e3f415168fc5dc1f720c932eb1ef4f71a2067e08b"}, + {file = "multidict-6.0.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b9d153e7f1f9ba0b23ad1568b3b9e17301e23b042c23870f9ee0522dc5cc79e8"}, + {file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:aef9cc3d9c7d63d924adac329c33835e0243b5052a6dfcbf7732a921c6e918ba"}, + {file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:4571f1beddff25f3e925eea34268422622963cd8dc395bb8778eb28418248e43"}, + {file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:d48b8ee1d4068561ce8033d2c344cf5232cb29ee1a0206a7b828c79cbc5982b8"}, + {file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:45183c96ddf61bf96d2684d9fbaf6f3564d86b34cb125761f9a0ef9e36c1d55b"}, + {file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:75bdf08716edde767b09e76829db8c1e5ca9d8bb0a8d4bd94ae1eafe3dac5e15"}, + {file = "multidict-6.0.2-cp37-cp37m-win32.whl", hash = "sha256:a45e1135cb07086833ce969555df39149680e5471c04dfd6a915abd2fc3f6dbc"}, + {file = "multidict-6.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6f3cdef8a247d1eafa649085812f8a310e728bdf3900ff6c434eafb2d443b23a"}, + {file = "multidict-6.0.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0327292e745a880459ef71be14e709aaea2f783f3537588fb4ed09b6c01bca60"}, + {file = "multidict-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e875b6086e325bab7e680e4316d667fc0e5e174bb5611eb16b3ea121c8951b86"}, + {file = "multidict-6.0.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:feea820722e69451743a3d56ad74948b68bf456984d63c1a92e8347b7b88452d"}, + {file = "multidict-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cc57c68cb9139c7cd6fc39f211b02198e69fb90ce4bc4a094cf5fe0d20fd8b0"}, + {file = "multidict-6.0.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:497988d6b6ec6ed6f87030ec03280b696ca47dbf0648045e4e1d28b80346560d"}, + {file = "multidict-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:89171b2c769e03a953d5969b2f272efa931426355b6c0cb508022976a17fd376"}, + {file = "multidict-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:684133b1e1fe91eda8fa7447f137c9490a064c6b7f392aa857bba83a28cfb693"}, + {file = "multidict-6.0.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd9fc9c4849a07f3635ccffa895d57abce554b467d611a5009ba4f39b78a8849"}, + {file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e07c8e79d6e6fd37b42f3250dba122053fddb319e84b55dd3a8d6446e1a7ee49"}, + {file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:4070613ea2227da2bfb2c35a6041e4371b0af6b0be57f424fe2318b42a748516"}, + {file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:47fbeedbf94bed6547d3aa632075d804867a352d86688c04e606971595460227"}, + {file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:5774d9218d77befa7b70d836004a768fb9aa4fdb53c97498f4d8d3f67bb9cfa9"}, + {file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2957489cba47c2539a8eb7ab32ff49101439ccf78eab724c828c1a54ff3ff98d"}, + {file = "multidict-6.0.2-cp38-cp38-win32.whl", hash = "sha256:e5b20e9599ba74391ca0cfbd7b328fcc20976823ba19bc573983a25b32e92b57"}, + {file = "multidict-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:8004dca28e15b86d1b1372515f32eb6f814bdf6f00952699bdeb541691091f96"}, + {file = "multidict-6.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2e4a0785b84fb59e43c18a015ffc575ba93f7d1dbd272b4cdad9f5134b8a006c"}, + {file = "multidict-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6701bf8a5d03a43375909ac91b6980aea74b0f5402fbe9428fc3f6edf5d9677e"}, + {file = "multidict-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a007b1638e148c3cfb6bf0bdc4f82776cef0ac487191d093cdc316905e504071"}, + {file = "multidict-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:07a017cfa00c9890011628eab2503bee5872f27144936a52eaab449be5eaf032"}, + {file = "multidict-6.0.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c207fff63adcdf5a485969131dc70e4b194327666b7e8a87a97fbc4fd80a53b2"}, + {file = "multidict-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:373ba9d1d061c76462d74e7de1c0c8e267e9791ee8cfefcf6b0b2495762c370c"}, + {file = "multidict-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfba7c6d5d7c9099ba21f84662b037a0ffd4a5e6b26ac07d19e423e6fdf965a9"}, + {file = "multidict-6.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19d9bad105dfb34eb539c97b132057a4e709919ec4dd883ece5838bcbf262b80"}, + {file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:de989b195c3d636ba000ee4281cd03bb1234635b124bf4cd89eeee9ca8fcb09d"}, + {file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7c40b7bbece294ae3a87c1bc2abff0ff9beef41d14188cda94ada7bcea99b0fb"}, + {file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:d16cce709ebfadc91278a1c005e3c17dd5f71f5098bfae1035149785ea6e9c68"}, + {file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:a2c34a93e1d2aa35fbf1485e5010337c72c6791407d03aa5f4eed920343dd360"}, + {file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:feba80698173761cddd814fa22e88b0661e98cb810f9f986c54aa34d281e4937"}, + {file = "multidict-6.0.2-cp39-cp39-win32.whl", hash = "sha256:23b616fdc3c74c9fe01d76ce0d1ce872d2d396d8fa8e4899398ad64fb5aa214a"}, + {file = "multidict-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:4bae31803d708f6f15fd98be6a6ac0b6958fcf68fda3c77a048a4f9073704aae"}, + {file = "multidict-6.0.2.tar.gz", hash = "sha256:5ff3bd75f38e4c43f1f470f2df7a4d430b821c4ce22be384e1459cb57d6bb013"}, +] +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-aiohttp = [ + {file = "pytest-aiohttp-1.0.4.tar.gz", hash = "sha256:39ff3a0d15484c01d1436cbedad575c6eafbf0f57cdf76fb94994c97b5b8c5a4"}, + {file = "pytest_aiohttp-1.0.4-py3-none-any.whl", hash = "sha256:1d2dc3a304c2be1fd496c0c2fb6b31ab60cd9fc33984f761f951f8ea1eb4ca95"}, +] +pytest-asyncio = [ + {file = "pytest-asyncio-0.18.3.tar.gz", hash = "sha256:7659bdb0a9eb9c6e3ef992eef11a2b3e69697800ad02fb06374a210d85b29f91"}, + {file = "pytest_asyncio-0.18.3-1-py3-none-any.whl", hash = "sha256:16cf40bdf2b4fb7fc8e4b82bd05ce3fbcd454cbf7b92afc445fe299dabb88213"}, + {file = "pytest_asyncio-0.18.3-py3-none-any.whl", hash = "sha256:8fafa6c52161addfd41ee7ab35f11836c5a16ec208f93ee388f752bea3493a84"}, +] +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"}, +] +sniffio = [ + {file = "sniffio-1.2.0-py3-none-any.whl", hash = "sha256:471b71698eac1c2112a40ce2752bb2f4a4814c22a54a3eed3676bc0f5ca9f663"}, + {file = "sniffio-1.2.0.tar.gz", hash = "sha256:c4666eecec1d3f50960c6bdf61ab7bc350648da6c126e3cf6898d8cd4ddcd3de"}, +] +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"}, +] +watchgod = [ + {file = "watchgod-0.8.2-py3-none-any.whl", hash = "sha256:2f3e8137d98f493ff58af54ea00f4d1433a6afe2ed08ab331a657df468c6bfce"}, + {file = "watchgod-0.8.2.tar.gz", hash = "sha256:cb11ff66657befba94d828e3b622d5fb76f22fbda1376f355f3e6e51e97d9450"}, +] +yarl = [ + {file = "yarl-1.7.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f2a8508f7350512434e41065684076f640ecce176d262a7d54f0da41d99c5a95"}, + {file = "yarl-1.7.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:da6df107b9ccfe52d3a48165e48d72db0eca3e3029b5b8cb4fe6ee3cb870ba8b"}, + {file = "yarl-1.7.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a1d0894f238763717bdcfea74558c94e3bc34aeacd3351d769460c1a586a8b05"}, + {file = "yarl-1.7.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dfe4b95b7e00c6635a72e2d00b478e8a28bfb122dc76349a06e20792eb53a523"}, + {file = "yarl-1.7.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c145ab54702334c42237a6c6c4cc08703b6aa9b94e2f227ceb3d477d20c36c63"}, + {file = "yarl-1.7.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1ca56f002eaf7998b5fcf73b2421790da9d2586331805f38acd9997743114e98"}, + {file = "yarl-1.7.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1d3d5ad8ea96bd6d643d80c7b8d5977b4e2fb1bab6c9da7322616fd26203d125"}, + {file = "yarl-1.7.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:167ab7f64e409e9bdd99333fe8c67b5574a1f0495dcfd905bc7454e766729b9e"}, + {file = "yarl-1.7.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:95a1873b6c0dd1c437fb3bb4a4aaa699a48c218ac7ca1e74b0bee0ab16c7d60d"}, + {file = "yarl-1.7.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6152224d0a1eb254f97df3997d79dadd8bb2c1a02ef283dbb34b97d4f8492d23"}, + {file = "yarl-1.7.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:5bb7d54b8f61ba6eee541fba4b83d22b8a046b4ef4d8eb7f15a7e35db2e1e245"}, + {file = "yarl-1.7.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:9c1f083e7e71b2dd01f7cd7434a5f88c15213194df38bc29b388ccdf1492b739"}, + {file = "yarl-1.7.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f44477ae29025d8ea87ec308539f95963ffdc31a82f42ca9deecf2d505242e72"}, + {file = "yarl-1.7.2-cp310-cp310-win32.whl", hash = "sha256:cff3ba513db55cc6a35076f32c4cdc27032bd075c9faef31fec749e64b45d26c"}, + {file = "yarl-1.7.2-cp310-cp310-win_amd64.whl", hash = "sha256:c9c6d927e098c2d360695f2e9d38870b2e92e0919be07dbe339aefa32a090265"}, + {file = "yarl-1.7.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:9b4c77d92d56a4c5027572752aa35082e40c561eec776048330d2907aead891d"}, + {file = "yarl-1.7.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c01a89a44bb672c38f42b49cdb0ad667b116d731b3f4c896f72302ff77d71656"}, + {file = "yarl-1.7.2-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c19324a1c5399b602f3b6e7db9478e5b1adf5cf58901996fc973fe4fccd73eed"}, + {file = "yarl-1.7.2-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3abddf0b8e41445426d29f955b24aeecc83fa1072be1be4e0d194134a7d9baee"}, + {file = "yarl-1.7.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6a1a9fe17621af43e9b9fcea8bd088ba682c8192d744b386ee3c47b56eaabb2c"}, + {file = "yarl-1.7.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8b0915ee85150963a9504c10de4e4729ae700af11df0dc5550e6587ed7891e92"}, + {file = "yarl-1.7.2-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:29e0656d5497733dcddc21797da5a2ab990c0cb9719f1f969e58a4abac66234d"}, + {file = "yarl-1.7.2-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:bf19725fec28452474d9887a128e98dd67eee7b7d52e932e6949c532d820dc3b"}, + {file = "yarl-1.7.2-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:d6f3d62e16c10e88d2168ba2d065aa374e3c538998ed04996cd373ff2036d64c"}, + {file = "yarl-1.7.2-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:ac10bbac36cd89eac19f4e51c032ba6b412b3892b685076f4acd2de18ca990aa"}, + {file = "yarl-1.7.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:aa32aaa97d8b2ed4e54dc65d241a0da1c627454950f7d7b1f95b13985afd6c5d"}, + {file = "yarl-1.7.2-cp36-cp36m-win32.whl", hash = "sha256:87f6e082bce21464857ba58b569370e7b547d239ca22248be68ea5d6b51464a1"}, + {file = "yarl-1.7.2-cp36-cp36m-win_amd64.whl", hash = "sha256:ac35ccde589ab6a1870a484ed136d49a26bcd06b6a1c6397b1967ca13ceb3913"}, + {file = "yarl-1.7.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a467a431a0817a292121c13cbe637348b546e6ef47ca14a790aa2fa8cc93df63"}, + {file = "yarl-1.7.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ab0c3274d0a846840bf6c27d2c60ba771a12e4d7586bf550eefc2df0b56b3b4"}, + {file = "yarl-1.7.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d260d4dc495c05d6600264a197d9d6f7fc9347f21d2594926202fd08cf89a8ba"}, + {file = "yarl-1.7.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fc4dd8b01a8112809e6b636b00f487846956402834a7fd59d46d4f4267181c41"}, + {file = "yarl-1.7.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c1164a2eac148d85bbdd23e07dfcc930f2e633220f3eb3c3e2a25f6148c2819e"}, + {file = "yarl-1.7.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:67e94028817defe5e705079b10a8438b8cb56e7115fa01640e9c0bb3edf67332"}, + {file = "yarl-1.7.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:89ccbf58e6a0ab89d487c92a490cb5660d06c3a47ca08872859672f9c511fc52"}, + {file = "yarl-1.7.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:8cce6f9fa3df25f55521fbb5c7e4a736683148bcc0c75b21863789e5185f9185"}, + {file = "yarl-1.7.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:211fcd65c58bf250fb994b53bc45a442ddc9f441f6fec53e65de8cba48ded986"}, + {file = "yarl-1.7.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c10ea1e80a697cf7d80d1ed414b5cb8f1eec07d618f54637067ae3c0334133c4"}, + {file = "yarl-1.7.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:52690eb521d690ab041c3919666bea13ab9fbff80d615ec16fa81a297131276b"}, + {file = "yarl-1.7.2-cp37-cp37m-win32.whl", hash = "sha256:695ba021a9e04418507fa930d5f0704edbce47076bdcfeeaba1c83683e5649d1"}, + {file = "yarl-1.7.2-cp37-cp37m-win_amd64.whl", hash = "sha256:c17965ff3706beedafd458c452bf15bac693ecd146a60a06a214614dc097a271"}, + {file = "yarl-1.7.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:fce78593346c014d0d986b7ebc80d782b7f5e19843ca798ed62f8e3ba8728576"}, + {file = "yarl-1.7.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c2a1ac41a6aa980db03d098a5531f13985edcb451bcd9d00670b03129922cd0d"}, + {file = "yarl-1.7.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:39d5493c5ecd75c8093fa7700a2fb5c94fe28c839c8e40144b7ab7ccba6938c8"}, + {file = "yarl-1.7.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1eb6480ef366d75b54c68164094a6a560c247370a68c02dddb11f20c4c6d3c9d"}, + {file = "yarl-1.7.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ba63585a89c9885f18331a55d25fe81dc2d82b71311ff8bd378fc8004202ff6"}, + {file = "yarl-1.7.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e39378894ee6ae9f555ae2de332d513a5763276a9265f8e7cbaeb1b1ee74623a"}, + {file = "yarl-1.7.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c0910c6b6c31359d2f6184828888c983d54d09d581a4a23547a35f1d0b9484b1"}, + {file = "yarl-1.7.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6feca8b6bfb9eef6ee057628e71e1734caf520a907b6ec0d62839e8293e945c0"}, + {file = "yarl-1.7.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8300401dc88cad23f5b4e4c1226f44a5aa696436a4026e456fe0e5d2f7f486e6"}, + {file = "yarl-1.7.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:788713c2896f426a4e166b11f4ec538b5736294ebf7d5f654ae445fd44270832"}, + {file = "yarl-1.7.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:fd547ec596d90c8676e369dd8a581a21227fe9b4ad37d0dc7feb4ccf544c2d59"}, + {file = "yarl-1.7.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:737e401cd0c493f7e3dd4db72aca11cfe069531c9761b8ea474926936b3c57c8"}, + {file = "yarl-1.7.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:baf81561f2972fb895e7844882898bda1eef4b07b5b385bcd308d2098f1a767b"}, + {file = "yarl-1.7.2-cp38-cp38-win32.whl", hash = "sha256:ede3b46cdb719c794427dcce9d8beb4abe8b9aa1e97526cc20de9bd6583ad1ef"}, + {file = "yarl-1.7.2-cp38-cp38-win_amd64.whl", hash = "sha256:cc8b7a7254c0fc3187d43d6cb54b5032d2365efd1df0cd1749c0c4df5f0ad45f"}, + {file = "yarl-1.7.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:580c1f15500e137a8c37053e4cbf6058944d4c114701fa59944607505c2fe3a0"}, + {file = "yarl-1.7.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3ec1d9a0d7780416e657f1e405ba35ec1ba453a4f1511eb8b9fbab81cb8b3ce1"}, + {file = "yarl-1.7.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3bf8cfe8856708ede6a73907bf0501f2dc4e104085e070a41f5d88e7faf237f3"}, + {file = "yarl-1.7.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1be4bbb3d27a4e9aa5f3df2ab61e3701ce8fcbd3e9846dbce7c033a7e8136746"}, + {file = "yarl-1.7.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:534b047277a9a19d858cde163aba93f3e1677d5acd92f7d10ace419d478540de"}, + {file = "yarl-1.7.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6ddcd80d79c96eb19c354d9dca95291589c5954099836b7c8d29278a7ec0bda"}, + {file = "yarl-1.7.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9bfcd43c65fbb339dc7086b5315750efa42a34eefad0256ba114cd8ad3896f4b"}, + {file = "yarl-1.7.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f64394bd7ceef1237cc604b5a89bf748c95982a84bcd3c4bbeb40f685c810794"}, + {file = "yarl-1.7.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:044daf3012e43d4b3538562da94a88fb12a6490652dbc29fb19adfa02cf72eac"}, + {file = "yarl-1.7.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:368bcf400247318382cc150aaa632582d0780b28ee6053cd80268c7e72796dec"}, + {file = "yarl-1.7.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:bab827163113177aee910adb1f48ff7af31ee0289f434f7e22d10baf624a6dfe"}, + {file = "yarl-1.7.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:0cba38120db72123db7c58322fa69e3c0efa933040ffb586c3a87c063ec7cae8"}, + {file = "yarl-1.7.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:59218fef177296451b23214c91ea3aba7858b4ae3306dde120224cfe0f7a6ee8"}, + {file = "yarl-1.7.2-cp39-cp39-win32.whl", hash = "sha256:1edc172dcca3f11b38a9d5c7505c83c1913c0addc99cd28e993efeaafdfaa18d"}, + {file = "yarl-1.7.2-cp39-cp39-win_amd64.whl", hash = "sha256:797c2c412b04403d2da075fb93c123df35239cd7b4cc4e0cd9e5839b73f52c58"}, + {file = "yarl-1.7.2.tar.gz", hash = "sha256:45399b46d60c253327a460e99856752009fcee5f5d3c80b2f7c0cae1c38d56dd"}, +] diff --git a/packages/bthlabs-jsonrpc-aiohttp/poetry.toml b/packages/bthlabs-jsonrpc-aiohttp/poetry.toml new file mode 100644 index 0000000..53b35d3 --- /dev/null +++ b/packages/bthlabs-jsonrpc-aiohttp/poetry.toml @@ -0,0 +1,3 @@ +[virtualenvs] +create = true +in-project = true diff --git a/packages/bthlabs-jsonrpc-aiohttp/pyproject.toml b/packages/bthlabs-jsonrpc-aiohttp/pyproject.toml new file mode 100644 index 0000000..aebcc0b --- /dev/null +++ b/packages/bthlabs-jsonrpc-aiohttp/pyproject.toml @@ -0,0 +1,32 @@ +[tool.poetry] +name = "bthlabs-jsonrpc-aiohttp" +version = "1.0.0" +description = "BTHLabs JSONRPC - aiohttp integration" +authors = ["Tomek Wójcik "] +maintainers = ["BTHLabs "] +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" diff --git a/packages/bthlabs-jsonrpc-aiohttp/setup.cfg b/packages/bthlabs-jsonrpc-aiohttp/setup.cfg new file mode 100644 index 0000000..7dd3637 --- /dev/null +++ b/packages/bthlabs-jsonrpc-aiohttp/setup.cfg @@ -0,0 +1,7 @@ +[flake8] +exclude = .venv/,.pytest_cache/ +ignore = E402 +max-line-length = 119 + +[tool:pytest] +asyncio_mode = auto diff --git a/packages/bthlabs-jsonrpc-aiohttp/skel/envrc b/packages/bthlabs-jsonrpc-aiohttp/skel/envrc new file mode 100644 index 0000000..d0a6f5d --- /dev/null +++ b/packages/bthlabs-jsonrpc-aiohttp/skel/envrc @@ -0,0 +1,2 @@ +export VIRTUAL_ENV="`realpath .venv`" +export PATH="$VIRTUAL_ENV/bin:$PATH" diff --git a/packages/bthlabs-jsonrpc-aiohttp/tests/__init__.py b/packages/bthlabs-jsonrpc-aiohttp/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/packages/bthlabs-jsonrpc-aiohttp/tests/conftest.py b/packages/bthlabs-jsonrpc-aiohttp/tests/conftest.py new file mode 100644 index 0000000..b3495c3 --- /dev/null +++ b/packages/bthlabs-jsonrpc-aiohttp/tests/conftest.py @@ -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) diff --git a/packages/bthlabs-jsonrpc-aiohttp/tests/executor/__init__.py b/packages/bthlabs-jsonrpc-aiohttp/tests/executor/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/packages/bthlabs-jsonrpc-aiohttp/tests/executor/test_AioHttpExecutor.py b/packages/bthlabs-jsonrpc-aiohttp/tests/executor/test_AioHttpExecutor.py new file mode 100644 index 0000000..d74be17 --- /dev/null +++ b/packages/bthlabs-jsonrpc-aiohttp/tests/executor/test_AioHttpExecutor.py @@ -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}, + ) diff --git a/packages/bthlabs-jsonrpc-aiohttp/tests/views/__init__.py b/packages/bthlabs-jsonrpc-aiohttp/tests/views/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/packages/bthlabs-jsonrpc-aiohttp/tests/views/test_JSONRPCView.py b/packages/bthlabs-jsonrpc-aiohttp/tests/views/test_JSONRPCView.py new file mode 100644 index 0000000..b8b4e92 --- /dev/null +++ b/packages/bthlabs-jsonrpc-aiohttp/tests/views/test_JSONRPCView.py @@ -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 diff --git a/packages/bthlabs-jsonrpc-core/LICENSE b/packages/bthlabs-jsonrpc-core/LICENSE new file mode 100644 index 0000000..d5b80fc --- /dev/null +++ b/packages/bthlabs-jsonrpc-core/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2022-present Tomek Wójcik + +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. diff --git a/packages/bthlabs-jsonrpc-core/README.rst b/packages/bthlabs-jsonrpc-core/README.rst new file mode 100644 index 0000000..a814254 --- /dev/null +++ b/packages/bthlabs-jsonrpc-core/README.rst @@ -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/ diff --git a/packages/bthlabs-jsonrpc-core/bthlabs_jsonrpc_core/__init__.py b/packages/bthlabs-jsonrpc-core/bthlabs_jsonrpc_core/__init__.py new file mode 100644 index 0000000..dcc6993 --- /dev/null +++ b/packages/bthlabs-jsonrpc-core/bthlabs_jsonrpc_core/__init__.py @@ -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' diff --git a/packages/bthlabs-jsonrpc-core/bthlabs_jsonrpc_core/decorators.py b/packages/bthlabs-jsonrpc-core/bthlabs_jsonrpc_core/decorators.py new file mode 100644 index 0000000..ac52a60 --- /dev/null +++ b/packages/bthlabs-jsonrpc-core/bthlabs_jsonrpc_core/decorators.py @@ -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 diff --git a/packages/bthlabs-jsonrpc-core/bthlabs_jsonrpc_core/exceptions.py b/packages/bthlabs-jsonrpc-core/bthlabs_jsonrpc_core/exceptions.py new file mode 100644 index 0000000..ec1130a --- /dev/null +++ b/packages/bthlabs-jsonrpc-core/bthlabs_jsonrpc_core/exceptions.py @@ -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' diff --git a/packages/bthlabs-jsonrpc-core/bthlabs_jsonrpc_core/executor.py b/packages/bthlabs-jsonrpc-core/bthlabs_jsonrpc_core/executor.py new file mode 100644 index 0000000..4c94e7a --- /dev/null +++ b/packages/bthlabs-jsonrpc-core/bthlabs_jsonrpc_core/executor.py @@ -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 diff --git a/packages/bthlabs-jsonrpc-core/bthlabs_jsonrpc_core/registry.py b/packages/bthlabs-jsonrpc-core/bthlabs_jsonrpc_core/registry.py new file mode 100644 index 0000000..70162ff --- /dev/null +++ b/packages/bthlabs-jsonrpc-core/bthlabs_jsonrpc_core/registry.py @@ -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) diff --git a/packages/bthlabs-jsonrpc-core/bthlabs_jsonrpc_core/serializer.py b/packages/bthlabs-jsonrpc-core/bthlabs_jsonrpc_core/serializer.py new file mode 100644 index 0000000..9c0cd11 --- /dev/null +++ b/packages/bthlabs-jsonrpc-core/bthlabs_jsonrpc_core/serializer.py @@ -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 diff --git a/packages/bthlabs-jsonrpc-core/docs/Makefile b/packages/bthlabs-jsonrpc-core/docs/Makefile new file mode 100644 index 0000000..d0c3cbf --- /dev/null +++ b/packages/bthlabs-jsonrpc-core/docs/Makefile @@ -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) diff --git a/packages/bthlabs-jsonrpc-core/docs/source/api.rst b/packages/bthlabs-jsonrpc-core/docs/source/api.rst new file mode 100644 index 0000000..9e8faa8 --- /dev/null +++ b/packages/bthlabs-jsonrpc-core/docs/source/api.rst @@ -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: diff --git a/packages/bthlabs-jsonrpc-core/docs/source/conf.py b/packages/bthlabs-jsonrpc-core/docs/source/conf.py new file mode 100644 index 0000000..a37e05f --- /dev/null +++ b/packages/bthlabs-jsonrpc-core/docs/source/conf.py @@ -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'] diff --git a/packages/bthlabs-jsonrpc-core/docs/source/index.rst b/packages/bthlabs-jsonrpc-core/docs/source/index.rst new file mode 100644 index 0000000..e296e1f --- /dev/null +++ b/packages/bthlabs-jsonrpc-core/docs/source/index.rst @@ -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 diff --git a/packages/bthlabs-jsonrpc-core/docs/source/integrations.rst b/packages/bthlabs-jsonrpc-core/docs/source/integrations.rst new file mode 100644 index 0000000..4008087 --- /dev/null +++ b/packages/bthlabs-jsonrpc-core/docs/source/integrations.rst @@ -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/ | ++-------------------+------------------------------------------------------+ diff --git a/packages/bthlabs-jsonrpc-core/docs/source/overview.rst b/packages/bthlabs-jsonrpc-core/docs/source/overview.rst new file mode 100644 index 0000000..69a5787 --- /dev/null +++ b/packages/bthlabs-jsonrpc-core/docs/source/overview.rst @@ -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. diff --git a/packages/bthlabs-jsonrpc-core/poetry.lock b/packages/bthlabs-jsonrpc-core/poetry.lock new file mode 100644 index 0000000..4d13b77 --- /dev/null +++ b/packages/bthlabs-jsonrpc-core/poetry.lock @@ -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"}, +] diff --git a/packages/bthlabs-jsonrpc-core/poetry.toml b/packages/bthlabs-jsonrpc-core/poetry.toml new file mode 100644 index 0000000..53b35d3 --- /dev/null +++ b/packages/bthlabs-jsonrpc-core/poetry.toml @@ -0,0 +1,3 @@ +[virtualenvs] +create = true +in-project = true diff --git a/packages/bthlabs-jsonrpc-core/pyproject.toml b/packages/bthlabs-jsonrpc-core/pyproject.toml new file mode 100644 index 0000000..94dd91a --- /dev/null +++ b/packages/bthlabs-jsonrpc-core/pyproject.toml @@ -0,0 +1,26 @@ +[tool.poetry] +name = "bthlabs-jsonrpc-core" +version = "1.0.0" +description = "BTHLabs JSONRPC - Core" +authors = ["Tomek Wójcik "] +maintainers = ["BTHLabs "] +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" diff --git a/packages/bthlabs-jsonrpc-core/setup.cfg b/packages/bthlabs-jsonrpc-core/setup.cfg new file mode 100644 index 0000000..565e3a9 --- /dev/null +++ b/packages/bthlabs-jsonrpc-core/setup.cfg @@ -0,0 +1,4 @@ +[flake8] +exclude = .venv/,.pytest_cache/ +ignore = E402 +max-line-length = 119 diff --git a/packages/bthlabs-jsonrpc-core/skel/envrc b/packages/bthlabs-jsonrpc-core/skel/envrc new file mode 100644 index 0000000..d0a6f5d --- /dev/null +++ b/packages/bthlabs-jsonrpc-core/skel/envrc @@ -0,0 +1,2 @@ +export VIRTUAL_ENV="`realpath .venv`" +export PATH="$VIRTUAL_ENV/bin:$PATH" diff --git a/packages/bthlabs-jsonrpc-core/tests/__init__.py b/packages/bthlabs-jsonrpc-core/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/packages/bthlabs-jsonrpc-core/tests/conftest.py b/packages/bthlabs-jsonrpc-core/tests/conftest.py new file mode 100644 index 0000000..e878203 --- /dev/null +++ b/packages/bthlabs-jsonrpc-core/tests/conftest.py @@ -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) diff --git a/packages/bthlabs-jsonrpc-core/tests/decorators/__init__.py b/packages/bthlabs-jsonrpc-core/tests/decorators/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/packages/bthlabs-jsonrpc-core/tests/decorators/test_register_method.py b/packages/bthlabs-jsonrpc-core/tests/decorators/test_register_method.py new file mode 100644 index 0000000..b8b3f48 --- /dev/null +++ b/packages/bthlabs-jsonrpc-core/tests/decorators/test_register_method.py @@ -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, + ) diff --git a/packages/bthlabs-jsonrpc-core/tests/exceptions/__init__.py b/packages/bthlabs-jsonrpc-core/tests/exceptions/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/packages/bthlabs-jsonrpc-core/tests/exceptions/test_BaseJSONRPCError.py b/packages/bthlabs-jsonrpc-core/tests/exceptions/test_BaseJSONRPCError.py new file mode 100644 index 0000000..d1723ce --- /dev/null +++ b/packages/bthlabs-jsonrpc-core/tests/exceptions/test_BaseJSONRPCError.py @@ -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 diff --git a/packages/bthlabs-jsonrpc-core/tests/executor/__init__.py b/packages/bthlabs-jsonrpc-core/tests/executor/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/packages/bthlabs-jsonrpc-core/tests/executor/test_Executor.py b/packages/bthlabs-jsonrpc-core/tests/executor/test_Executor.py new file mode 100644 index 0000000..48a607d --- /dev/null +++ b/packages/bthlabs-jsonrpc-core/tests/executor/test_Executor.py @@ -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}, + ) diff --git a/packages/bthlabs-jsonrpc-core/tests/registry/__init__.py b/packages/bthlabs-jsonrpc-core/tests/registry/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/packages/bthlabs-jsonrpc-core/tests/registry/test_MethodRedistry.py b/packages/bthlabs-jsonrpc-core/tests/registry/test_MethodRedistry.py new file mode 100644 index 0000000..1bcd91f --- /dev/null +++ b/packages/bthlabs-jsonrpc-core/tests/registry/test_MethodRedistry.py @@ -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 diff --git a/packages/bthlabs-jsonrpc-core/tests/serializer/__init__.py b/packages/bthlabs-jsonrpc-core/tests/serializer/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/packages/bthlabs-jsonrpc-core/tests/serializer/test_JSONRPCSerializer.py b/packages/bthlabs-jsonrpc-core/tests/serializer/test_JSONRPCSerializer.py new file mode 100644 index 0000000..95d4a11 --- /dev/null +++ b/packages/bthlabs-jsonrpc-core/tests/serializer/test_JSONRPCSerializer.py @@ -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' diff --git a/packages/bthlabs-jsonrpc-django/LICENSE b/packages/bthlabs-jsonrpc-django/LICENSE new file mode 100644 index 0000000..2ccbe66 --- /dev/null +++ b/packages/bthlabs-jsonrpc-django/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2023-present Tomek Wójcik + +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. diff --git a/packages/bthlabs-jsonrpc-django/README.rst b/packages/bthlabs-jsonrpc-django/README.rst new file mode 100644 index 0000000..0b1f9b8 --- /dev/null +++ b/packages/bthlabs-jsonrpc-django/README.rst @@ -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/ diff --git a/packages/bthlabs-jsonrpc-django/bthlabs_jsonrpc_django/__init__.py b/packages/bthlabs-jsonrpc-django/bthlabs_jsonrpc_django/__init__.py new file mode 100644 index 0000000..a249169 --- /dev/null +++ b/packages/bthlabs-jsonrpc-django/bthlabs_jsonrpc_django/__init__.py @@ -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' diff --git a/packages/bthlabs-jsonrpc-django/bthlabs_jsonrpc_django/apps.py b/packages/bthlabs-jsonrpc-django/bthlabs_jsonrpc_django/apps.py new file mode 100644 index 0000000..bfc32f1 --- /dev/null +++ b/packages/bthlabs-jsonrpc-django/bthlabs_jsonrpc_django/apps.py @@ -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) diff --git a/packages/bthlabs-jsonrpc-django/bthlabs_jsonrpc_django/auth_checks.py b/packages/bthlabs-jsonrpc-django/bthlabs_jsonrpc_django/auth_checks.py new file mode 100644 index 0000000..d2061a9 --- /dev/null +++ b/packages/bthlabs-jsonrpc-django/bthlabs_jsonrpc_django/auth_checks.py @@ -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 diff --git a/packages/bthlabs-jsonrpc-django/bthlabs_jsonrpc_django/executor.py b/packages/bthlabs-jsonrpc-django/bthlabs_jsonrpc_django/executor.py new file mode 100644 index 0000000..b421bd8 --- /dev/null +++ b/packages/bthlabs-jsonrpc-django/bthlabs_jsonrpc_django/executor.py @@ -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') diff --git a/packages/bthlabs-jsonrpc-django/bthlabs_jsonrpc_django/serializer.py b/packages/bthlabs-jsonrpc-django/bthlabs_jsonrpc_django/serializer.py new file mode 100644 index 0000000..fa7aa27 --- /dev/null +++ b/packages/bthlabs-jsonrpc-django/bthlabs_jsonrpc_django/serializer.py @@ -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) diff --git a/packages/bthlabs-jsonrpc-django/bthlabs_jsonrpc_django/views.py b/packages/bthlabs-jsonrpc-django/bthlabs_jsonrpc_django/views.py new file mode 100644 index 0000000..6f9fae9 --- /dev/null +++ b/packages/bthlabs-jsonrpc-django/bthlabs_jsonrpc_django/views.py @@ -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 diff --git a/packages/bthlabs-jsonrpc-django/docs/Makefile b/packages/bthlabs-jsonrpc-django/docs/Makefile new file mode 100644 index 0000000..d0c3cbf --- /dev/null +++ b/packages/bthlabs-jsonrpc-django/docs/Makefile @@ -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) diff --git a/packages/bthlabs-jsonrpc-django/docs/source/api.rst b/packages/bthlabs-jsonrpc-django/docs/source/api.rst new file mode 100644 index 0000000..e87eaa0 --- /dev/null +++ b/packages/bthlabs-jsonrpc-django/docs/source/api.rst @@ -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 diff --git a/packages/bthlabs-jsonrpc-django/docs/source/conf.py b/packages/bthlabs-jsonrpc-django/docs/source/conf.py new file mode 100644 index 0000000..56f7f8f --- /dev/null +++ b/packages/bthlabs-jsonrpc-django/docs/source/conf.py @@ -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'] diff --git a/packages/bthlabs-jsonrpc-django/docs/source/index.rst b/packages/bthlabs-jsonrpc-django/docs/source/index.rst new file mode 100644 index 0000000..e77a661 --- /dev/null +++ b/packages/bthlabs-jsonrpc-django/docs/source/index.rst @@ -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 diff --git a/packages/bthlabs-jsonrpc-django/docs/source/overview.rst b/packages/bthlabs-jsonrpc-django/docs/source/overview.rst new file mode 100644 index 0000000..26b6900 --- /dev/null +++ b/packages/bthlabs-jsonrpc-django/docs/source/overview.rst @@ -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}!' diff --git a/packages/bthlabs-jsonrpc-django/example/.gitignore b/packages/bthlabs-jsonrpc-django/example/.gitignore new file mode 100644 index 0000000..3eae80e --- /dev/null +++ b/packages/bthlabs-jsonrpc-django/example/.gitignore @@ -0,0 +1 @@ +/db.sqlite3 diff --git a/packages/bthlabs-jsonrpc-django/example/bthlabs_jsonrpc_django_example/__init__.py b/packages/bthlabs-jsonrpc-django/example/bthlabs_jsonrpc_django_example/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/packages/bthlabs-jsonrpc-django/example/bthlabs_jsonrpc_django_example/asgi.py b/packages/bthlabs-jsonrpc-django/example/bthlabs_jsonrpc_django_example/asgi.py new file mode 100644 index 0000000..b56eb21 --- /dev/null +++ b/packages/bthlabs-jsonrpc-django/example/bthlabs_jsonrpc_django_example/asgi.py @@ -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() diff --git a/packages/bthlabs-jsonrpc-django/example/bthlabs_jsonrpc_django_example/middleware.py b/packages/bthlabs-jsonrpc-django/example/bthlabs_jsonrpc_django_example/middleware.py new file mode 100644 index 0000000..25462ee --- /dev/null +++ b/packages/bthlabs-jsonrpc-django/example/bthlabs_jsonrpc_django_example/middleware.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +from django.contrib.auth.middleware import RemoteUserMiddleware + + +class CustomHeaderRemoteUserMiddleware(RemoteUserMiddleware): + header = 'HTTP_X_USER' diff --git a/packages/bthlabs-jsonrpc-django/example/bthlabs_jsonrpc_django_example/settings/.gitignore b/packages/bthlabs-jsonrpc-django/example/bthlabs_jsonrpc_django_example/settings/.gitignore new file mode 100644 index 0000000..75276c8 --- /dev/null +++ b/packages/bthlabs-jsonrpc-django/example/bthlabs_jsonrpc_django_example/settings/.gitignore @@ -0,0 +1 @@ +/local.py diff --git a/packages/bthlabs-jsonrpc-django/example/bthlabs_jsonrpc_django_example/settings/__init__.py b/packages/bthlabs-jsonrpc-django/example/bthlabs_jsonrpc_django_example/settings/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/packages/bthlabs-jsonrpc-django/example/bthlabs_jsonrpc_django_example/settings/_base.py b/packages/bthlabs-jsonrpc-django/example/bthlabs_jsonrpc_django_example/settings/_base.py new file mode 100644 index 0000000..3a93b5f --- /dev/null +++ b/packages/bthlabs-jsonrpc-django/example/bthlabs_jsonrpc_django_example/settings/_base.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', +] diff --git a/packages/bthlabs-jsonrpc-django/example/bthlabs_jsonrpc_django_example/things/__init__.py b/packages/bthlabs-jsonrpc-django/example/bthlabs_jsonrpc_django_example/things/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/packages/bthlabs-jsonrpc-django/example/bthlabs_jsonrpc_django_example/things/admin.py b/packages/bthlabs-jsonrpc-django/example/bthlabs_jsonrpc_django_example/things/admin.py new file mode 100644 index 0000000..c5f70b3 --- /dev/null +++ b/packages/bthlabs-jsonrpc-django/example/bthlabs_jsonrpc_django_example/things/admin.py @@ -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) diff --git a/packages/bthlabs-jsonrpc-django/example/bthlabs_jsonrpc_django_example/things/apps.py b/packages/bthlabs-jsonrpc-django/example/bthlabs_jsonrpc_django_example/things/apps.py new file mode 100644 index 0000000..c5db3f2 --- /dev/null +++ b/packages/bthlabs-jsonrpc-django/example/bthlabs_jsonrpc_django_example/things/apps.py @@ -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' diff --git a/packages/bthlabs-jsonrpc-django/example/bthlabs_jsonrpc_django_example/things/migrations/0001_initial.py b/packages/bthlabs-jsonrpc-django/example/bthlabs_jsonrpc_django_example/things/migrations/0001_initial.py new file mode 100644 index 0000000..322c102 --- /dev/null +++ b/packages/bthlabs-jsonrpc-django/example/bthlabs_jsonrpc_django_example/things/migrations/0001_initial.py @@ -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', + }, + ), + ] diff --git a/packages/bthlabs-jsonrpc-django/example/bthlabs_jsonrpc_django_example/things/migrations/__init__.py b/packages/bthlabs-jsonrpc-django/example/bthlabs_jsonrpc_django_example/things/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/packages/bthlabs-jsonrpc-django/example/bthlabs_jsonrpc_django_example/things/models.py b/packages/bthlabs-jsonrpc-django/example/bthlabs_jsonrpc_django_example/things/models.py new file mode 100644 index 0000000..a2d5034 --- /dev/null +++ b/packages/bthlabs-jsonrpc-django/example/bthlabs_jsonrpc_django_example/things/models.py @@ -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, + } diff --git a/packages/bthlabs-jsonrpc-django/example/bthlabs_jsonrpc_django_example/things/rpc_methods.py b/packages/bthlabs-jsonrpc-django/example/bthlabs_jsonrpc_django_example/things/rpc_methods.py new file mode 100644 index 0000000..c98e110 --- /dev/null +++ b/packages/bthlabs-jsonrpc-django/example/bthlabs_jsonrpc_django_example/things/rpc_methods.py @@ -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 diff --git a/packages/bthlabs-jsonrpc-django/example/bthlabs_jsonrpc_django_example/urls.py b/packages/bthlabs-jsonrpc-django/example/bthlabs_jsonrpc_django_example/urls.py new file mode 100644 index 0000000..b6b06d9 --- /dev/null +++ b/packages/bthlabs-jsonrpc-django/example/bthlabs_jsonrpc_django_example/urls.py @@ -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()), +] diff --git a/packages/bthlabs-jsonrpc-django/example/bthlabs_jsonrpc_django_example/wsgi.py b/packages/bthlabs-jsonrpc-django/example/bthlabs_jsonrpc_django_example/wsgi.py new file mode 100644 index 0000000..554c006 --- /dev/null +++ b/packages/bthlabs-jsonrpc-django/example/bthlabs_jsonrpc_django_example/wsgi.py @@ -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() diff --git a/packages/bthlabs-jsonrpc-django/example/manage.py b/packages/bthlabs-jsonrpc-django/example/manage.py new file mode 100755 index 0000000..8d2f242 --- /dev/null +++ b/packages/bthlabs-jsonrpc-django/example/manage.py @@ -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() diff --git a/packages/bthlabs-jsonrpc-django/poetry.lock b/packages/bthlabs-jsonrpc-django/poetry.lock new file mode 100644 index 0000000..e2485c6 --- /dev/null +++ b/packages/bthlabs-jsonrpc-django/poetry.lock @@ -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"}, +] diff --git a/packages/bthlabs-jsonrpc-django/poetry.toml b/packages/bthlabs-jsonrpc-django/poetry.toml new file mode 100644 index 0000000..53b35d3 --- /dev/null +++ b/packages/bthlabs-jsonrpc-django/poetry.toml @@ -0,0 +1,3 @@ +[virtualenvs] +create = true +in-project = true diff --git a/packages/bthlabs-jsonrpc-django/pyproject.toml b/packages/bthlabs-jsonrpc-django/pyproject.toml new file mode 100644 index 0000000..e78b441 --- /dev/null +++ b/packages/bthlabs-jsonrpc-django/pyproject.toml @@ -0,0 +1,32 @@ +[tool.poetry] +name = "bthlabs-jsonrpc-django" +version = "1.0.0" +description = "BTHLabs JSONRPC - Django integration" +authors = ["Tomek Wójcik "] +maintainers = ["BTHLabs "] +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" diff --git a/packages/bthlabs-jsonrpc-django/setup.cfg b/packages/bthlabs-jsonrpc-django/setup.cfg new file mode 100644 index 0000000..4a13f5a --- /dev/null +++ b/packages/bthlabs-jsonrpc-django/setup.cfg @@ -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 diff --git a/packages/bthlabs-jsonrpc-django/skel/envrc b/packages/bthlabs-jsonrpc-django/skel/envrc new file mode 100644 index 0000000..d0a6f5d --- /dev/null +++ b/packages/bthlabs-jsonrpc-django/skel/envrc @@ -0,0 +1,2 @@ +export VIRTUAL_ENV="`realpath .venv`" +export PATH="$VIRTUAL_ENV/bin:$PATH" diff --git a/packages/bthlabs-jsonrpc-django/testing/.gitignore b/packages/bthlabs-jsonrpc-django/testing/.gitignore new file mode 100644 index 0000000..3eae80e --- /dev/null +++ b/packages/bthlabs-jsonrpc-django/testing/.gitignore @@ -0,0 +1 @@ +/db.sqlite3 diff --git a/packages/bthlabs-jsonrpc-django/testing/__init__.py b/packages/bthlabs-jsonrpc-django/testing/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/packages/bthlabs-jsonrpc-django/testing/apps.py b/packages/bthlabs-jsonrpc-django/testing/apps.py new file mode 100644 index 0000000..957b7dc --- /dev/null +++ b/packages/bthlabs-jsonrpc-django/testing/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class TestingConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'testing' diff --git a/packages/bthlabs-jsonrpc-django/testing/factories.py b/packages/bthlabs-jsonrpc-django/testing/factories.py new file mode 100644 index 0000000..2d813dd --- /dev/null +++ b/packages/bthlabs-jsonrpc-django/testing/factories.py @@ -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 diff --git a/packages/bthlabs-jsonrpc-django/testing/migrations/0001_initial.py b/packages/bthlabs-jsonrpc-django/testing/migrations/0001_initial.py new file mode 100644 index 0000000..f17cad4 --- /dev/null +++ b/packages/bthlabs-jsonrpc-django/testing/migrations/0001_initial.py @@ -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', + }, + ), + ] diff --git a/packages/bthlabs-jsonrpc-django/testing/migrations/__init__.py b/packages/bthlabs-jsonrpc-django/testing/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/packages/bthlabs-jsonrpc-django/testing/models.py b/packages/bthlabs-jsonrpc-django/testing/models.py new file mode 100644 index 0000000..278489c --- /dev/null +++ b/packages/bthlabs-jsonrpc-django/testing/models.py @@ -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, + } diff --git a/packages/bthlabs-jsonrpc-django/testing/settings.py b/packages/bthlabs-jsonrpc-django/testing/settings.py new file mode 100644 index 0000000..e8d86e6 --- /dev/null +++ b/packages/bthlabs-jsonrpc-django/testing/settings.py @@ -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 = [ +] diff --git a/packages/bthlabs-jsonrpc-django/testing/urls.py b/packages/bthlabs-jsonrpc-django/testing/urls.py new file mode 100644 index 0000000..6f9529e --- /dev/null +++ b/packages/bthlabs-jsonrpc-django/testing/urls.py @@ -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()), +] diff --git a/packages/bthlabs-jsonrpc-django/tests/__init_.py b/packages/bthlabs-jsonrpc-django/tests/__init_.py new file mode 100644 index 0000000..e69de29 diff --git a/packages/bthlabs-jsonrpc-django/tests/auth_checks/__init__.py b/packages/bthlabs-jsonrpc-django/tests/auth_checks/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/packages/bthlabs-jsonrpc-django/tests/auth_checks/test_has_perms.py b/packages/bthlabs-jsonrpc-django/tests/auth_checks/test_has_perms.py new file mode 100644 index 0000000..0844437 --- /dev/null +++ b/packages/bthlabs-jsonrpc-django/tests/auth_checks/test_has_perms.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +from unittest import mock + +from bthlabs_jsonrpc_django import auth_checks + + +def test_has_perms_regular_user(rf, user): + # Given + request = rf.get('/') + request.user = user + + check = auth_checks.has_perms(['can_use_rpc']) + + # When + result = check(request) + + # Then + assert result is False + + +def test_has_perms_ok(rf, user): + # Given + request = rf.get('/') + request.user = user + + check = auth_checks.has_perms(['can_use_rpc']) + + with mock.patch.object(user, 'has_perms') as mock_has_perms: + mock_has_perms.return_value = True + + # When + result = check(request) + + # Then + assert result is True + + mock_has_perms.assert_called_with(['can_use_rpc']) + + +def test_has_perms_ok_super_user(rf, super_user): + # Given + request = rf.get('/') + request.user = super_user + + check = auth_checks.has_perms(['can_use_rpc']) + + # When + result = check(request) + + # Then + assert result is True diff --git a/packages/bthlabs-jsonrpc-django/tests/auth_checks/test_is_authenticated.py b/packages/bthlabs-jsonrpc-django/tests/auth_checks/test_is_authenticated.py new file mode 100644 index 0000000..003c388 --- /dev/null +++ b/packages/bthlabs-jsonrpc-django/tests/auth_checks/test_is_authenticated.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +from django.contrib.auth.models import AnonymousUser + +from bthlabs_jsonrpc_django import auth_checks + + +def test_is_authenticated_anonymous_user(rf): + # Given + request = rf.get('/') + request.user = AnonymousUser() + + # When + result = auth_checks.is_authenticated(request) + + # Then + assert result is False + + +def test_is_authenticated_inactive(rf, inactive_user): + # Given + request = rf.get('/') + request.user = inactive_user + + # When + result = auth_checks.is_authenticated(request) + + # Then + assert result is False + + +def test_is_authenticated_ok(rf, user): + # Given + request = rf.get('/') + request.user = user + + # When + result = auth_checks.is_authenticated(request) + + # Then + assert result is True diff --git a/packages/bthlabs-jsonrpc-django/tests/auth_checks/test_is_staff.py b/packages/bthlabs-jsonrpc-django/tests/auth_checks/test_is_staff.py new file mode 100644 index 0000000..46340d4 --- /dev/null +++ b/packages/bthlabs-jsonrpc-django/tests/auth_checks/test_is_staff.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +from bthlabs_jsonrpc_django import auth_checks + + +def test_is_staff_regular_user(rf, user): + # Given + request = rf.get('/') + request.user = user + + # When + result = auth_checks.is_staff(request) + + # Then + assert result is False + + +def test_is_staff_ok(rf, staff_user): + # Given + request = rf.get('/') + request.user = staff_user + + # When + result = auth_checks.is_staff(request) + + # Then + assert result is True diff --git a/packages/bthlabs-jsonrpc-django/tests/conftest.py b/packages/bthlabs-jsonrpc-django/tests/conftest.py new file mode 100644 index 0000000..2330d47 --- /dev/null +++ b/packages/bthlabs-jsonrpc-django/tests/conftest.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +from django.contrib.auth.models import User +import factory +import pytest + + +class UserFactory(factory.django.DjangoModelFactory): + username = factory.Faker('email') + first_name = factory.Faker('first_name') + last_name = factory.Faker('last_name') + email = factory.Faker('email') + is_staff = False + is_superuser = False + is_active = True + + class Meta: + model = User + + +@pytest.fixture +def user(db): + return UserFactory() + + +@pytest.fixture +def inactive_user(db): + return UserFactory(is_active=False) + + +@pytest.fixture +def staff_user(db): + return UserFactory(is_staff=True) + + +@pytest.fixture +def super_user(db): + return UserFactory(is_superuser=True) + + +@pytest.fixture +def call(): + return { + 'jsonrpc': '2.0', + 'id': 'system.list_methods', + 'method': 'system.list_methods', + } diff --git a/packages/bthlabs-jsonrpc-django/tests/executor/__init__.py b/packages/bthlabs-jsonrpc-django/tests/executor/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/packages/bthlabs-jsonrpc-django/tests/executor/test_DjangoExecutor.py b/packages/bthlabs-jsonrpc-django/tests/executor/test_DjangoExecutor.py new file mode 100644 index 0000000..723e2fa --- /dev/null +++ b/packages/bthlabs-jsonrpc-django/tests/executor/test_DjangoExecutor.py @@ -0,0 +1,67 @@ +# -*- coding: utf-8 -*- +from unittest import mock + +from bthlabs_jsonrpc_core import exceptions +import pytest + +from bthlabs_jsonrpc_django import executor + + +@pytest.fixture +def fake_can_call(): + return mock.Mock() + + +def test_init(rf, fake_can_call): + # Given + request = rf.get('/') + + # When + result = executor.DjangoExecutor(request, fake_can_call) + + # Then + assert result.request == request + assert result.can_call == fake_can_call + + +def test_enrich_args(rf, fake_can_call): + # Given + request = rf.get('/') + + the_executor = executor.DjangoExecutor(request, fake_can_call) + + # When + result = the_executor.enrich_args(['spam']) + + # Then + assert result == [request, 'spam'] + + +def test_before_call(rf, fake_can_call): + # Given + request = rf.get('/') + + the_executor = executor.DjangoExecutor(request, fake_can_call) + + # When + the_executor.before_call('test', ['spam'], {'spam': True}) + + # Then + fake_can_call.assert_called_with(request, 'test', ['spam'], {'spam': True}) + + +def test_before_call_access_denied(rf, fake_can_call): + # Given + fake_can_call.return_value = False + + request = rf.get('/') + + the_executor = executor.DjangoExecutor(request, fake_can_call) + + # When + try: + the_executor.before_call('test', ['spam'], {'spam': True}) + except Exception as exception: + assert isinstance(exception, exceptions.JSONRPCAccessDeniedError) + else: + assert False, 'No exception raised?' diff --git a/packages/bthlabs-jsonrpc-django/tests/serializer/test_DjangoJSONRPCSerializer.py b/packages/bthlabs-jsonrpc-django/tests/serializer/test_DjangoJSONRPCSerializer.py new file mode 100644 index 0000000..6ac630b --- /dev/null +++ b/packages/bthlabs-jsonrpc-django/tests/serializer/test_DjangoJSONRPCSerializer.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +import pytest + +from bthlabs_jsonrpc_django import serializer +from testing.factories import ThingFactory +from testing.models import Thing + + +@pytest.mark.django_db +def test_serialize_value_query_set(): + # Given + things = [ThingFactory() for _ in range(0, 3)] + + query_set = Thing.objects.\ + filter(pk__in=[thing.pk for thing in things]).\ + order_by('pk') + + the_serializer = serializer.DjangoJSONRPCSerializer('spam') + + # When + result = the_serializer.serialize_value(query_set) + + # Then + assert isinstance(result, list) + assert len(result) == 3 + + expected_serialized_thing = things[0].to_rpc() + expected_serialized_thing.update({ + 'created_at': expected_serialized_thing['created_at'].isoformat(), + 'modified_at': expected_serialized_thing['modified_at'].isoformat(), + }) + assert result[0] == expected_serialized_thing diff --git a/packages/bthlabs-jsonrpc-django/tests/views/__init__.py b/packages/bthlabs-jsonrpc-django/tests/views/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/packages/bthlabs-jsonrpc-django/tests/views/test_JSONRPCView.py b/packages/bthlabs-jsonrpc-django/tests/views/test_JSONRPCView.py new file mode 100644 index 0000000..b4bcbf4 --- /dev/null +++ b/packages/bthlabs-jsonrpc-django/tests/views/test_JSONRPCView.py @@ -0,0 +1,91 @@ +# -*- coding: utf-8 -*- +from bthlabs_jsonrpc_core import exceptions + + +def test_view(client): + # Given + 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 = client.post('/rpc', data=batch, content_type='application/json') + + # Then + assert response.status_code == 200 + + data = 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 + + +def test_view_empty_response(client, call): + # Given + call.pop('id') + + # When + response = client.post('/rpc', data=call, content_type='application/json') + + # Then + assert response.status_code == 200 + assert response.content == b'' + + +def test_view_with_auth_checks(client, user, call): + # Given + client.force_login(user) + + # When + response = client.post( + '/rpc/private', data=call, content_type='application/json', + ) + + # Then + assert response.status_code == 200 + + data = response.json() + expected_result_data = { + 'jsonrpc': '2.0', + 'id': 'system.list_methods', + 'result': ['system.list_methods'], + } + assert data == expected_result_data + + +def test_view_with_auth_checks_permission_denied(client, call): + # When + response = client.post( + '/rpc/private', data=call, content_type='application/json', + ) + + # Then + assert response.status_code == 403